Merge pull request #56 from lordmathis/fix/body-already-read

Fix double read of json response when content-length header is missing
This commit is contained in:
2025-10-04 22:28:22 +02:00
committed by GitHub
4 changed files with 21 additions and 14 deletions

View File

@@ -131,11 +131,16 @@ func (h *Handler) ListInstances() http.HandlerFunc {
return return
} }
w.Header().Set("Content-Type", "application/json") // Marshal to bytes first to set Content-Length header
if err := json.NewEncoder(w).Encode(instances); err != nil { data, err := json.Marshal(instances)
if err != nil {
http.Error(w, "Failed to encode instances: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to encode instances: "+err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Write(data)
} }
} }

View File

@@ -11,11 +11,13 @@ describe('API Error Handling', () => {
}) })
it('converts HTTP errors to meaningful messages', async () => { it('converts HTTP errors to meaningful messages', async () => {
mockFetch.mockResolvedValue({ const mockResponse = {
ok: false, ok: false,
status: 409, status: 409,
text: () => Promise.resolve('Instance already exists') text: () => Promise.resolve('Instance already exists'),
}) clone: function() { return this }
}
mockFetch.mockResolvedValue(mockResponse)
await expect(instancesApi.create('existing', {})) await expect(instancesApi.create('existing', {}))
.rejects .rejects
@@ -23,11 +25,13 @@ describe('API Error Handling', () => {
}) })
it('handles empty error responses gracefully', async () => { it('handles empty error responses gracefully', async () => {
mockFetch.mockResolvedValue({ const mockResponse = {
ok: false, ok: false,
status: 500, status: 500,
text: () => Promise.resolve('') text: () => Promise.resolve(''),
}) clone: function() { return this }
}
mockFetch.mockResolvedValue(mockResponse)
await expect(instancesApi.list()) await expect(instancesApi.list())
.rejects .rejects

View File

@@ -49,11 +49,8 @@ async function apiCall<T>(
} else { } else {
// Handle empty responses for JSON endpoints // Handle empty responses for JSON endpoints
const contentLength = response.headers.get('content-length'); const contentLength = response.headers.get('content-length');
if (contentLength === '0' || contentLength === null) { if (contentLength === '0') {
const text = await response.text(); return {} as T; // Return empty object for empty JSON responses
if (text.trim() === '') {
return {} as T; // Return empty object for empty JSON responses
}
} }
const data = await response.json() as T; const data = await response.json() as T;
return data; return data;

View File

@@ -26,7 +26,8 @@ export async function handleApiError(response: Response): Promise<void> {
} }
if (!response.ok) { if (!response.ok) {
const errorMessage = await parseErrorResponse(response) // Clone the response before reading to avoid consuming the body stream
const errorMessage = await parseErrorResponse(response.clone())
throw new Error(errorMessage) throw new Error(errorMessage)
} }
} }