Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 89 additions & 46 deletions docs/src/components/api/enhanced-api-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type RequestState = {
headers?: Record<string, string>,
loading: boolean,
error?: string,
errorDetails?: {
url?: string,
method?: string,
type?: 'network' | 'cors' | 'timeout' | 'unknown',
},
timestamp?: number,
duration?: number,
},
Expand Down Expand Up @@ -262,19 +267,18 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
};

// Build request body from individual fields
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && Object.keys(requestState.bodyFields).length > 0) {
// Filter out empty values and build JSON body
const bodyData = Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
);
if (Object.keys(bodyData).length > 0) {
requestOptions.body = JSON.stringify(bodyData);
// Add Content-Type header when sending JSON body
requestOptions.headers = {
...filteredHeaders,
'Content-Type': 'application/json',
};
}
// Always send a body for POST/PUT/PATCH - even if empty, some endpoints require it
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
const bodyData = operation.requestBody
? Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
)
: {};
requestOptions.body = JSON.stringify(bodyData);
requestOptions.headers = {
...filteredHeaders,
'Content-Type': 'application/json',
};
}

const response = await fetch(url, requestOptions);
Expand All @@ -298,16 +302,35 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
}
}));
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Request failed';
const rawMessage = err instanceof Error ? err.message : 'Request failed';

// Determine error type and create helpful message
let errorType: 'network' | 'cors' | 'timeout' | 'unknown' = 'unknown';
let errorMessage = rawMessage;

if (rawMessage === 'Failed to fetch' || rawMessage.includes('NetworkError')) {
errorType = 'network';
errorMessage = 'Network error: Unable to reach the API server.\n\nThis could be due to:\n• CORS policy blocking the request\n• The API server being unreachable\n• A network connectivity issue';
} else if (rawMessage.includes('CORS') || rawMessage.includes('cross-origin')) {
errorType = 'cors';
errorMessage = 'CORS error: The API server rejected this cross-origin request.\n\nThe API may not allow requests from this domain.';
} else if (rawMessage.includes('timeout') || rawMessage.includes('Timeout')) {
errorType = 'timeout';
errorMessage = 'Request timed out: The API server took too long to respond.';
}

// Report network errors as well
reportError(0, { message: errorMessage });
reportError(0, { message: rawMessage });

setRequestState(prev => ({
...prev,
response: {
loading: false,
error: errorMessage,
errorDetails: {
method: method.toUpperCase(),
type: errorType,
},
Comment on lines +330 to +333
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errorDetails type defines a url field (line 31), and the PR description states that error responses include "URL, HTTP method, and error type classification," but the implementation never populates the URL field.

View Details
📝 Patch Details
diff --git a/docs/src/components/api/enhanced-api-page.tsx b/docs/src/components/api/enhanced-api-page.tsx
index 90a6fd68..33a7fb6a 100644
--- a/docs/src/components/api/enhanced-api-page.tsx
+++ b/docs/src/components/api/enhanced-api-page.tsx
@@ -328,6 +328,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA
           loading: false,
           error: errorMessage,
           errorDetails: {
+            url: url,
             method: method.toUpperCase(),
             type: errorType,
           },

Analysis

Missing URL field in error details metadata for API playground

What fails: EnhancedAPIPage in docs/src/components/api/enhanced-api-page.tsx defines an errorDetails type with an optional url field (line 30), but the catch block handler (lines 330-333) only populates method and type fields, leaving url undefined despite it being available in scope.

How to reproduce:

  1. In the API playground component, trigger a network error (e.g., fetch fails due to network issue)
  2. The error response object's errorDetails.url property will be undefined
  3. Expected: errorDetails.url should contain the constructed request URL with all path parameters and query parameters resolved

Current behavior: errorDetails object contains only method and type:

errorDetails: {
  method: method.toUpperCase(),
  type: errorType,
}

Expected behavior: errorDetails object should include the URL:

errorDetails: {
  url: url,
  method: method.toUpperCase(),
  type: errorType,
}

Root cause: The url variable is constructed and available in the executeRequest function (defined at line 236 in the try block), with path parameters and query parameters resolved before the fetch call. However, when error handling occurs in the catch block, the URL field was not included in the errorDetails metadata object despite being defined in the type schema, suggesting incomplete implementation of the feature for capturing comprehensive error context.

Impact: While the URL field is not currently rendered in the UI, including it in the error metadata would enable future debugging capabilities and align with the complete error context design indicated by the type definition.

timestamp: startTime,
duration: Date.now() - startTime,
}
Expand Down Expand Up @@ -451,14 +474,16 @@ function ModernAPIPlayground({
}
});

// Add body for POST/PUT/PATCH - build from fields
if (['POST', 'PUT', 'PATCH'].includes(method) && Object.keys(requestState.bodyFields).length > 0) {
const bodyData = Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
);
if (Object.keys(bodyData).length > 0) {
curlCommand += ` \\\n -d '${JSON.stringify(bodyData)}'`;
}
// Add body for POST/PUT/PATCH - always include Content-Type and body
// Even if empty, some endpoints require a JSON body to be present
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const bodyData = operation.requestBody
? Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
)
: {};
curlCommand += ` \\\n -H "Content-Type: application/json"`;
curlCommand += ` \\\n -d '${JSON.stringify(bodyData)}'`;
}

return curlCommand;
Expand Down Expand Up @@ -496,22 +521,29 @@ function ModernAPIPlayground({
Object.entries(requestState.headers).filter(([key, value]) => key && value)
);

// Add body for POST/PUT/PATCH - always include Content-Type and body
// Even if empty, some endpoints require a JSON body to be present
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const bodyData = operation.requestBody
? Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
)
: {};
const headersWithContentType = { ...headers, 'Content-Type': 'application/json' };

let jsCode = `const response = await fetch("${url}", {\n method: "${method}"`;
jsCode += `,\n headers: ${JSON.stringify(headersWithContentType, null, 4).replace(/^/gm, ' ')}`;
jsCode += `,\n body: JSON.stringify(${JSON.stringify(bodyData, null, 2)})`;
jsCode += `\n});\n\nconst data = await response.json();\nconsole.log(data);`;
return jsCode;
}

let jsCode = `const response = await fetch("${url}", {\n method: "${method}"`;

if (Object.keys(headers).length > 0) {
jsCode += `,\n headers: ${JSON.stringify(headers, null, 4).replace(/^/gm, ' ')}`;
}

// Add body for POST/PUT/PATCH - build from fields
if (['POST', 'PUT', 'PATCH'].includes(method) && Object.keys(requestState.bodyFields).length > 0) {
const bodyData = Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
);
if (Object.keys(bodyData).length > 0) {
jsCode += `,\n body: ${JSON.stringify(bodyData, null, 2)}`;
}
}

jsCode += `\n});\n\nconst data = await response.json();\nconsole.log(data);`;

return jsCode;
Expand Down Expand Up @@ -557,17 +589,16 @@ function ModernAPIPlayground({
pythonCode += `headers = ${JSON.stringify(headers, null, 2).replace(/"/g, "'")}\n`;
}

// Add body for POST/PUT/PATCH - build from fields
if (['POST', 'PUT', 'PATCH'].includes(method) && Object.keys(requestState.bodyFields).length > 0) {
const bodyData = Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
);
if (Object.keys(bodyData).length > 0) {
pythonCode += `data = ${JSON.stringify(bodyData)}\n\n`;
pythonCode += `response = requests.${method.toLowerCase()}(url${Object.keys(headers).length > 0 ? ', headers=headers' : ''}, json=data)\n`;
} else {
pythonCode += `\nresponse = requests.${method.toLowerCase()}(url${Object.keys(headers).length > 0 ? ', headers=headers' : ''})\n`;
}
// Add body for POST/PUT/PATCH - always include json body
// Even if empty, some endpoints require a JSON body to be present
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const bodyData = operation.requestBody
? Object.fromEntries(
Object.entries(requestState.bodyFields).filter(([, value]) => value !== '' && value !== undefined)
)
: {};
pythonCode += `data = ${JSON.stringify(bodyData)}\n\n`;
pythonCode += `response = requests.${method.toLowerCase()}(url${Object.keys(headers).length > 0 ? ', headers=headers' : ''}, json=data)\n`;
} else {
pythonCode += `\nresponse = requests.${method.toLowerCase()}(url${Object.keys(headers).length > 0 ? ', headers=headers' : ''})\n`;
}
Expand Down Expand Up @@ -1236,9 +1267,21 @@ function ResponsePanel({
<p className="text-fd-muted-foreground text-sm text-center leading-relaxed m-0">Sending request...</p>
</div>
) : response.error ? (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-red-800 dark:text-red-300 font-medium mb-2 leading-none">Request Failed</p>
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 space-y-3">
<div className="flex items-center gap-2">
<span className="text-red-800 dark:text-red-300 font-medium leading-none">Request Failed</span>
{response.errorDetails?.type && response.errorDetails.type !== 'unknown' && (
<span className="text-xs bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 px-2 py-0.5 rounded font-mono uppercase">
{response.errorDetails.type}
</span>
)}
</div>
<p className="text-red-600 dark:text-red-400 text-sm whitespace-pre-wrap break-words leading-relaxed m-0">{response.error}</p>
{response.duration && (
<p className="text-red-500/70 dark:text-red-400/70 text-xs m-0">
Failed after {response.duration}ms
</p>
)}
</div>
) : response.status ? (
<div className="space-y-4">
Expand Down
Loading