[Docs] fix broken requests on API playground (#1125)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* API error responses now include richer metadata (URL, HTTP method, and
inferred error type).
* Error panels show an error type badge (when known) and request
duration for failed calls.
* POST/PUT/PATCH requests consistently send JSON bodies and include
Content-Type headers, even when empty.
* Code examples (curl/JS/Python) updated to reflect consistent body and
header handling.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Madison 2026-02-04 09:42:57 -06:00 committed by GitHub
parent bb69ee4230
commit 5b811fa012
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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,
},
@ -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);
@ -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,
},
timestamp: startTime,
duration: Date.now() - startTime,
}
@ -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;
@ -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;
@ -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`;
}
@ -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">