Skip to content

Handle 200-with-empty-body responses in shared HTTP client#12

Open
andreibondarev wants to merge 1 commit into
lzinga:mainfrom
andreibondarev:fix/empty-body-json-parse
Open

Handle 200-with-empty-body responses in shared HTTP client#12
andreibondarev wants to merge 1 commit into
lzinga:mainfrom
andreibondarev:fix/empty-body-json-parse

Conversation

@andreibondarev

@andreibondarev andreibondarev commented May 5, 2026

Copy link
Copy Markdown

Description

Some upstream APIs return HTTP 200 with an empty body when there are no rows matching the filter. The shared HTTP client called await res.json() directly, which throws SyntaxError: Unexpected end of JSON input. That bubbles out of tool.execute() as a confusing error instead of an empty result.

Reproduced via dol_osha_accidents with event_keyword: "keyword":

{"content":"Tool 'dol_osha_accidents' execution failed: Unexpected end of JSON input"}

The companion tool dol_osha_inspections handles "no results" cleanly (returns the standard emptyResponse), so the inconsistency is purely in the client's body-parsing path.

This change reads the body as text first in src/shared/client.ts:

  • If the body is empty or whitespace-only, returns null (the existing Array.isArray(data) || !data.length checks in tool wrappers already handle this and emit emptyResponse).
  • If the body is non-empty but not valid JSON, throws a clearer error that includes the request name, status code, and a 200-char body preview — much friendlier to debug than a bare Unexpected end of JSON input.
  • Valid JSON still flows through unchanged.

Adds vitest coverage for all four cases (empty, whitespace, non-JSON, valid JSON).

Type of Change

  • Bug fix

Verification

  • npm run build — passes
  • npm test — 926/926 passed (922 existing + 4 new)

Additional Notes

The fix is in shared client code, so it benefits every module that uses createClient (FRED, DOL, BLS, etc.) — not just DOL. If any other API silently returns empty bodies, those will now degrade to emptyResponse instead of throwing.

I considered making the JSON-parse error a soft warning + null rather than a throw, but the current behavior (rejecting non-JSON 200s as errors) is probably what most callers want, and changing that would be a behavior change beyond the scope of this fix.

Some upstream APIs (e.g. DOL OSHA when no rows match the filter)
return HTTP 200 with an empty body. The client called
`await res.json()` directly, which throws "Unexpected end of JSON
input" and bubbles out of tool execute() as a confusing error
instead of an empty result.

This reads the body as text first, returns null for empty/
whitespace-only responses, and wraps JSON.parse with a clearer
error message that includes a body preview when the response is
non-JSON. Adds vitest coverage for the four cases (empty,
whitespace, non-JSON, valid JSON).

Reproduced via: dol_osha_accidents with event_keyword=fentanyl.
@andreibondarev

Copy link
Copy Markdown
Author

@lzinga Thank you for building this.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the shared createClient HTTP client so that upstream APIs returning HTTP 200 with an empty (or whitespace-only) body no longer cause a JSON parse crash, and adds unit tests to validate the new behavior.

Changes:

  • Update createClient to read successful responses as text first, returning null for empty/whitespace-only bodies and emitting a clearer error for non-JSON bodies.
  • Add vitest coverage for empty body, whitespace-only body, non-JSON body, and valid JSON body scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/shared/client.ts Adjusts the success-path body parsing to handle empty bodies safely and improve parse error diagnostics.
tests/client-empty-body.test.ts Adds unit tests covering the new empty/invalid body parsing behavior in the shared client.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +19
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createClient } from "../src/shared/client.js";

describe("createClient — empty/invalid body handling", () => {
const originalFetch = globalThis.fetch;

beforeEach(() => {
// Keep cache effects out of these tests by varying the URL per case.
});

Comment on lines +30 to +35
const client = createClient({
baseUrl: "https://example.test",
name: "empty-body-test",
});

const data = await client.get<unknown>("/empty");
Comment thread src/shared/client.ts
Comment on lines +538 to +542
const raw = await res.text();
let data: unknown;
if (raw.trim() === "") {
data = null;
} else {
Comment thread src/shared/client.ts
Comment on lines +547 to +548
throw new Error(
`${name}: invalid JSON response (HTTP ${res.status}): ${(err as Error).message}. Body: ${preview}`,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants