mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[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:
parent
bb69ee4230
commit
5b811fa012
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user