Files
supabase/apps/studio/data/normalizeEmptyBodyResponse.test.ts
Ali Waseem ee3bec08af fix: intercept responses missing content lenght and re-add (#46885)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Cases with cloudflares http/3 the content-length header is optional, so
in many cases we need to make sure in this case `openapi-fetch` can
safely parse this (i.e ignore when the body is empty and no header is
present)

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

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Fixed JSON parsing failures when successful API responses contain
empty bodies without `Content-Length` headers. Improves compatibility
with HTTP/3 and similar response types.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-12 11:16:12 -06:00

103 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('@sentry/nextjs', () => ({ captureException: vi.fn() }))
vi.mock('common', () => ({ IS_PLATFORM: false, getAccessToken: vi.fn() }))
vi.mock('@/lib/constants', () => ({ API_URL: 'http://localhost' }))
vi.mock('@/lib/helpers', () => ({ uuidv4: () => 'test-uuid' }))
// Import after mocks are set up
const { normalizeEmptyBodyResponse, client } = await import('./fetchers')
describe('normalizeEmptyBodyResponse', () => {
it('adds Content-Length: 0 to an empty 201 that omits it (HTTP/3 case)', async () => {
// HTTP/3 responses can drop `Content-Length: 0` on empty bodies, which makes
// openapi-fetch attempt to JSON-parse the empty body and throw.
const response = new Response(null, { status: 201 })
expect(response.headers.has('Content-Length')).toBe(false)
const normalized = await normalizeEmptyBodyResponse(response)
expect(normalized.status).toBe(201)
expect(normalized.headers.get('Content-Length')).toBe('0')
// The normalized empty body must not throw when parsed the way openapi-fetch parses it.
expect(await normalized.text()).toBe('')
})
it('leaves a response that already has Content-Length untouched (HTTP/2 case)', async () => {
const response = new Response(null, {
status: 201,
headers: { 'Content-Length': '0' },
})
const normalized = await normalizeEmptyBodyResponse(response)
expect(normalized).toBe(response)
})
it('does not modify a non-empty body that omits Content-Length', async () => {
const response = new Response(JSON.stringify({ id: 1 }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
expect(response.headers.has('Content-Length')).toBe(false)
const normalized = await normalizeEmptyBodyResponse(response)
// Returns the original response (body still readable, not consumed by the peek).
expect(normalized).toBe(response)
expect(await normalized.json()).toEqual({ id: 1 })
})
it('leaves 204 No Content untouched even without Content-Length', async () => {
const response = new Response(null, { status: 204 })
const normalized = await normalizeEmptyBodyResponse(response)
expect(normalized).toBe(response)
})
it('preserves status and statusText when normalizing', async () => {
const response = new Response(null, { status: 201, statusText: 'Created' })
const normalized = await normalizeEmptyBodyResponse(response)
expect(normalized.status).toBe(201)
expect(normalized.statusText).toBe('Created')
})
it('preserves other headers when adding Content-Length', async () => {
const response = new Response(null, {
status: 201,
headers: { 'X-Request-Id': 'req-123' },
})
const normalized = await normalizeEmptyBodyResponse(response)
expect(normalized.headers.get('X-Request-Id')).toBe('req-123')
expect(normalized.headers.get('Content-Length')).toBe('0')
})
})
describe('openapi-fetch client — empty 201 without Content-Length (HTTP/3)', () => {
afterEach(() => vi.unstubAllGlobals())
it('resolves with data instead of throwing on JSON parse', async () => {
// Simulate the HTTP/3 transport: a 201 with an empty body and no Content-Length.
// Without normalization openapi-fetch calls response.json() and throws
// "Unexpected end of JSON input"; the middleware should make this succeed.
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(null, { status: 201 }))
)
const { data, error, response } = await client.POST('/v1/projects/{ref}/secrets' as any, {
params: { path: { ref: 'test-ref' } },
body: [] as any,
})
expect(error).toBeUndefined()
expect(data).toEqual({})
expect(response.status).toBe(201)
})
})