Skip to content

[Bug] overrideGlobalObjects: true breaks openapi-fetch middleware instanceof checks #321

@haleygu64

Description

@haleygu64

What version of Hono are you using?

  • hono: 4.11.7
  • @hono/node-server: 1.19.9

What runtime/OS are you seeing the problem on?

  • Bun 1.2.19 (Linux)
  • Also reproduced on Node.js v22.17.1

What steps can reproduce the bug?

When @hono/node-server's serve() runs with overrideGlobalObjects: true (the default), it replaces globalThis.Request and globalThis.Response with lightweight polyfills. Libraries that validate objects via instanceof Request or instanceof Response break silently because the objects returned by native fetch() were constructed with the original classes, not Hono's replacements.

Minimal reproduction with openapi-fetch@0.14.1:

import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import createClient, { type Middleware } from 'openapi-fetch';

type Paths = {
  '/api/v1/data': {
    get: {
      responses: { 200: { content: { 'application/json': { ok: boolean } } } };
    };
  };
};

const apiClient = createClient<Paths>({ baseUrl: 'http://localhost:19100' });

// Standard logging middleware — returns the same request/response unmodified
const logMiddleware: Middleware = {
  async onRequest({ request }) {
    console.log(`--> ${request.method} ${request.url}`);
    return request;
  },
  async onResponse({ response }) {
    console.log(`<-- ${response.status} ${response.url}`);
    return response;
  },
};
apiClient.use(logMiddleware);

const app = new Hono();
app.get('/proxy', async (c) => {
  const { data, error } = await apiClient.GET('/api/v1/data');
  if (error) return c.json({ error }, 502);
  return c.json(data);
});

// overrideGlobalObjects defaults to true — this is the trigger
serve({ fetch: app.fetch, port: 3000 });

Upstream mock (any server returning JSON on :19100).

Trigger:

curl http://localhost:3000/proxy

Bun test proving the issue

import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test';
import { Hono } from 'hono';
import { serve, type ServerType } from '@hono/node-server';
import createClient, { type Middleware } from 'openapi-fetch';

type Paths = {
  '/api/v1/secrets': {
    get: {
      responses: {
        200: {
          content: { 'application/json': { data: Record<string, string> } };
        };
      };
    };
  };
};

const MOCK_PORT = 19_100;
const CLIENT_PORT = 19_101;
const OriginalRequest = globalThis.Request;
const OriginalResponse = globalThis.Response;

const middleware: Middleware = {
  async onRequest({ request }) {
    console.log(`--> ${request.method} ${request.url}`);
    return request;
  },
  async onResponse({ response }) {
    console.log(`<-- ${response.status} ${response.url}`);
    return response;
  },
};

function buildMockApp() {
  const app = new Hono();
  app.get('/api/v1/secrets', (c) =>
    c.json({ data: { apiKey: 'sk-live-XXX', dbPassword: 'secret' } }),
  );
  return app;
}

describe('overrideGlobalObjects breaks instanceof checks', () => {
  let mockServer: ReturnType<typeof Bun.serve>;
  let clientServer: ServerType | undefined;

  beforeAll(() => {
    mockServer = Bun.serve({ port: MOCK_PORT, fetch: buildMockApp().fetch });
  });
  afterEach(() => {
    clientServer?.close();
    clientServer = undefined;
    globalThis.Request = OriginalRequest;
    globalThis.Response = OriginalResponse;
  });
  afterAll(() => { mockServer?.stop(); });

  test('overrideGlobalObjects: true → middleware instanceof check fails', async () => {
    const apiClient = createClient<Paths>({ baseUrl: `http://localhost:${MOCK_PORT}` });
    apiClient.use(middleware);

    const app = new Hono();
    app.get('/fetch', async (c) => {
      try {
        const { data, error } = await apiClient.GET('/api/v1/secrets');
        if (error) return c.json({ error: 'upstream' }, 502);
        return c.json(data);
      } catch (err) {
        return c.json({ error: 'middleware_failure', message: String(err) }, 500);
      }
    });

    clientServer = serve({ fetch: app.fetch, port: CLIENT_PORT }); // default: overrideGlobalObjects true
    await Bun.sleep(200);

    expect(globalThis.Response).not.toBe(OriginalResponse); // confirms override

    const res = await fetch(`http://localhost:${CLIENT_PORT}/fetch`);
    const body = await res.json() as { error: string; message: string };
    expect(res.status).toBe(500);
    expect(body.message).toContain('onResponse: must return new Response() when modifying the response');
  });

  test('overrideGlobalObjects: false → same middleware works fine', async () => {
    const apiClient = createClient<Paths>({ baseUrl: `http://localhost:${MOCK_PORT}` });
    apiClient.use(middleware);

    const app = new Hono();
    app.get('/fetch', async (c) => {
      const { data, error } = await apiClient.GET('/api/v1/secrets');
      if (error) return c.json({ error: 'upstream' }, 502);
      return c.json(data);
    });

    clientServer = serve({ fetch: app.fetch, port: CLIENT_PORT, overrideGlobalObjects: false });
    await Bun.sleep(200);

    const res = await fetch(`http://localhost:${CLIENT_PORT}/fetch`);
    const body = await res.json() as { data: Record<string, string> };
    expect(res.status).toBe(200);
    expect(body.data.apiKey).toBe('sk-live-XXX');
  });
});

What is the expected behavior?

serve() should not break instanceof checks in third-party libraries. Returning the same Request/Response object from middleware callbacks should work regardless of overrideGlobalObjects.

What do you see instead?

Error: onResponse: must return new Response() when modifying the response
    at coreFetch (openapi-fetch/dist/index.mjs:165:23)

openapi-fetch internally validates middleware returns with:

if (result) {
  if (!(result instanceof Response)) {
    throw new Error("onResponse: must return new Response() when modifying the response");
  }
}

The Response object returned by native fetch() was constructed with the original Response class, but after overrideGlobalObjects, instanceof checks against Hono's replacement polyfill — which fails.

Root cause

@hono/node-server's overrideGlobalObjects replaces globalThis.Request and globalThis.Response with lightweight alternatives. Any library relying on instanceof Request or instanceof Response to validate objects created by the native fetch() will break, because native fetch constructs objects using the original constructors, not the replacements.

Additional context

This affects any library that uses instanceof Request or instanceof Response for validation, not just openapi-fetch. The pattern is common in HTTP middleware ecosystems (ky, wretch, custom fetch wrappers, etc.).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions