From 50a3963fa2d96ce60c96147c31e1f5cb25d1ed93 Mon Sep 17 00:00:00 2001 From: DmitryAnansky Date: Mon, 11 May 2026 12:09:57 +0300 Subject: [PATCH 1/4] fix: mismatch in respect status code when har-output option used --- .changeset/chilly-banks-act.md | 5 ++ .../respect/har-logs/with-har.test.ts | 49 +++++++++++++++++++ .../src/commands/respect/har-logs/with-har.ts | 5 +- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changeset/chilly-banks-act.md create mode 100644 packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts diff --git a/.changeset/chilly-banks-act.md b/.changeset/chilly-banks-act.md new file mode 100644 index 0000000000..0ab9da7900 --- /dev/null +++ b/.changeset/chilly-banks-act.md @@ -0,0 +1,5 @@ +--- +"@redocly/cli": patch +--- + +Fixed a status code mismatch that occurred when using the '--har-output' option in the Respect command. diff --git a/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts b/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts new file mode 100644 index 0000000000..2d403788e3 --- /dev/null +++ b/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts @@ -0,0 +1,49 @@ +import { createHarLog } from '../../../../commands/respect/har-logs/har-logs.js'; +import { withHar } from '../../../../commands/respect/har-logs/with-har.js'; + +describe('withHar', () => { + it('should preserve the original response status', async () => { + const har = createHarLog({ version: '1.0.0' }); + const dispatcher = { on: vi.fn() }; + const baseFetch = vi.fn(async () => { + return new Response(JSON.stringify({ created: true }), { + status: 201, + statusText: 'Created', + headers: { 'content-type': 'application/json' }, + }); + }); + + const fetch = withHar(baseFetch as any, { har }); + const response = await fetch('https://example.com/resources', { + method: 'POST', + dispatcher, + }); + + expect(response.status).toBe(201); + expect(response.statusText).toBe('Created'); + expect(await response.json()).toEqual({ created: true }); + expect(har.log.entries[0].response.status).toBe(201); + }); + + it('should return a bodyless response for no-content statuses', async () => { + const har = createHarLog({ version: '1.0.0' }); + const dispatcher = { on: vi.fn() }; + const baseFetch = vi.fn(async () => { + return new Response(null, { + status: 204, + statusText: 'No Content', + }); + }); + + const fetch = withHar(baseFetch as any, { har }); + const response = await fetch('https://example.com/resources', { + method: 'POST', + dispatcher, + }); + + expect(response.status).toBe(204); + expect(response.statusText).toBe('No Content'); + expect(await response.text()).toBe(''); + expect(har.log.entries[0].response.status).toBe(204); + }); +}); diff --git a/packages/cli/src/commands/respect/har-logs/with-har.ts b/packages/cli/src/commands/respect/har-logs/with-har.ts index ad17101a7b..933f24821e 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -17,6 +17,7 @@ import { buildResponseCookies } from './helpers/build-response-cookies.js'; import { getDuration } from './helpers/get-duration.js'; const HAR_HEADER_NAME = 'x-har-request-id'; +const NULL_BODY_STATUS_CODES = new Set([204, 205, 304]); const harEntryMap = new Map(); export interface WithHar { (baseFetch: T, defaults?: any): T; @@ -213,8 +214,8 @@ export const withHar: WithHar = function ( const Response = defaults.Response || baseFetch.Response || global.Response || response.constructor; - const responseCopy = new Response(text, { - status: response.statusCode, + const responseCopy = new Response(NULL_BODY_STATUS_CODES.has(response.status) ? null : text, { + status: response.status, statusText: response.statusText || '', headers: response.headers, url: response.url, From b14a347482532b7360001796ace88360bf427691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81=C4=99kawa?= <164185257+JLekawa@users.noreply.github.com> Date: Mon, 11 May 2026 11:31:44 +0200 Subject: [PATCH 2/4] Apply suggestion from @JLekawa --- .changeset/chilly-banks-act.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilly-banks-act.md b/.changeset/chilly-banks-act.md index 0ab9da7900..87ddfd3ffa 100644 --- a/.changeset/chilly-banks-act.md +++ b/.changeset/chilly-banks-act.md @@ -2,4 +2,4 @@ "@redocly/cli": patch --- -Fixed a status code mismatch that occurred when using the '--har-output' option in the Respect command. +Fixed a status code mismatch that occurred when using the `--har-output` option in the `respect` command. From 16a6c3052572bcb32ceb0fc1825623a8ed7445ba Mon Sep 17 00:00:00 2001 From: DmitryAnansky Date: Mon, 11 May 2026 12:52:42 +0300 Subject: [PATCH 3/4] chore: use cloned response body --- .../respect/har-logs/with-har.test.ts | 22 +++++++++++-------- .../src/commands/respect/har-logs/with-har.ts | 22 ++++++------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts b/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts index 2d403788e3..013821d3a8 100644 --- a/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts +++ b/packages/cli/src/__tests__/commands/respect/har-logs/with-har.test.ts @@ -5,12 +5,13 @@ describe('withHar', () => { it('should preserve the original response status', async () => { const har = createHarLog({ version: '1.0.0' }); const dispatcher = { on: vi.fn() }; + const originalResponse = new Response(JSON.stringify({ created: true }), { + status: 201, + statusText: 'Created', + headers: { 'content-type': 'application/json' }, + }); const baseFetch = vi.fn(async () => { - return new Response(JSON.stringify({ created: true }), { - status: 201, - statusText: 'Created', - headers: { 'content-type': 'application/json' }, - }); + return originalResponse; }); const fetch = withHar(baseFetch as any, { har }); @@ -19,6 +20,7 @@ describe('withHar', () => { dispatcher, }); + expect(response).toBe(originalResponse); expect(response.status).toBe(201); expect(response.statusText).toBe('Created'); expect(await response.json()).toEqual({ created: true }); @@ -28,11 +30,12 @@ describe('withHar', () => { it('should return a bodyless response for no-content statuses', async () => { const har = createHarLog({ version: '1.0.0' }); const dispatcher = { on: vi.fn() }; + const originalResponse = new Response(null, { + status: 204, + statusText: 'No Content', + }); const baseFetch = vi.fn(async () => { - return new Response(null, { - status: 204, - statusText: 'No Content', - }); + return originalResponse; }); const fetch = withHar(baseFetch as any, { har }); @@ -41,6 +44,7 @@ describe('withHar', () => { dispatcher, }); + expect(response).toBe(originalResponse); expect(response.status).toBe(204); expect(response.statusText).toBe('No Content'); expect(await response.text()).toBe(''); diff --git a/packages/cli/src/commands/respect/har-logs/with-har.ts b/packages/cli/src/commands/respect/har-logs/with-har.ts index 933f24821e..c2a6b77b8a 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -17,7 +17,6 @@ import { buildResponseCookies } from './helpers/build-response-cookies.js'; import { getDuration } from './helpers/get-duration.js'; const HAR_HEADER_NAME = 'x-har-request-id'; -const NULL_BODY_STATUS_CODES = new Set([204, 205, 304]); const harEntryMap = new Map(); export interface WithHar { (baseFetch: T, defaults?: any): T; @@ -111,14 +110,15 @@ export const withHar: WithHar = function ( // Make the request const response = await baseFetch(input, options); - // Need to clone response to get both text and arrayBuffer - const responseClone = response.clone(); + // Read from clones so HAR logging does not consume or reconstruct the real response. + const responseTextClone = response.clone(); + const responseBodyClone = response.clone(); // Update firstByte time when we get the response entry._timestamps.firstByte = process.hrtime(); // Get the response body and update received time - const text = await response.text(); + const text = response.body === null ? '' : await responseTextClone.text(); entry._timestamps.received = process.hrtime(); const harEntry = harEntryMap.get(requestId); @@ -150,7 +150,7 @@ export const withHar: WithHar = function ( } if (harEntry._compressed) { - const rawBody = await responseClone.arrayBuffer(); + const rawBody = await responseBodyClone.arrayBuffer(); harEntry.response.content.size = rawBody.byteLength; } else { harEntry.response.content.size = text ? Buffer.byteLength(text) : -1; @@ -212,15 +212,7 @@ export const withHar: WithHar = function ( parent.pageref = entry.pageref; }); - const Response = - defaults.Response || baseFetch.Response || global.Response || response.constructor; - const responseCopy = new Response(NULL_BODY_STATUS_CODES.has(response.status) ? null : text, { - status: response.status, - statusText: response.statusText || '', - headers: response.headers, - url: response.url, - }); - responseCopy.harEntry = entry; + response.harEntry = entry; if (Array.isArray(har?.log?.entries)) { har.log.entries.push(...parents, entry); @@ -233,6 +225,6 @@ export const withHar: WithHar = function ( onHarEntry(entry); } - return responseCopy; + return response; } as T; }; From 2f2c2585386173d3acd36284d7e7fcc795133e62 Mon Sep 17 00:00:00 2001 From: DmitryAnansky Date: Mon, 11 May 2026 12:56:15 +0300 Subject: [PATCH 4/4] chore: renames --- packages/cli/src/commands/respect/har-logs/with-har.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/respect/har-logs/with-har.ts b/packages/cli/src/commands/respect/har-logs/with-har.ts index c2a6b77b8a..08def49927 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -112,7 +112,7 @@ export const withHar: WithHar = function ( // Read from clones so HAR logging does not consume or reconstruct the real response. const responseTextClone = response.clone(); - const responseBodyClone = response.clone(); + const responseBufferClone = response.clone(); // Update firstByte time when we get the response entry._timestamps.firstByte = process.hrtime(); @@ -150,7 +150,7 @@ export const withHar: WithHar = function ( } if (harEntry._compressed) { - const rawBody = await responseBodyClone.arrayBuffer(); + const rawBody = await responseBufferClone.arrayBuffer(); harEntry.response.content.size = rawBody.byteLength; } else { harEntry.response.content.size = text ? Buffer.byteLength(text) : -1;