From 8c6570245aa4568fbd5c04820a859290f209807f Mon Sep 17 00:00:00 2001 From: Aries Clark Date: Thu, 30 Jul 2026 12:05:18 -0400 Subject: [PATCH 1/9] fix(respect): record request bodies in the HAR postData entry --- .changeset/respect-har-post-data.md | 5 ++ .../har-logs/helpers/build-post-data.test.ts | 60 +++++++++++++++++++ .../har-logs/helpers/build-post-data.ts | 35 +++++++++++ .../src/commands/respect/har-logs/with-har.ts | 6 +- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 .changeset/respect-har-post-data.md create mode 100644 packages/cli/src/__tests__/commands/respect/har-logs/helpers/build-post-data.test.ts create mode 100644 packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts diff --git a/.changeset/respect-har-post-data.md b/.changeset/respect-har-post-data.md new file mode 100644 index 0000000000..fe3750b77d --- /dev/null +++ b/.changeset/respect-har-post-data.md @@ -0,0 +1,5 @@ +--- +'@redocly/cli': patch +--- + +Fixed `respect --har-output` recording an empty `postData` for every request. Request bodies are now written to the HAR, so a capture replayed through `drift` can have its request bodies validated instead of silently passing. diff --git a/packages/cli/src/__tests__/commands/respect/har-logs/helpers/build-post-data.test.ts b/packages/cli/src/__tests__/commands/respect/har-logs/helpers/build-post-data.test.ts new file mode 100644 index 0000000000..d9332f2253 --- /dev/null +++ b/packages/cli/src/__tests__/commands/respect/har-logs/helpers/build-post-data.test.ts @@ -0,0 +1,60 @@ +import { buildPostData } from '../../../../../commands/respect/har-logs/helpers/build-post-data.js'; + +describe('buildPostData', () => { + it('records a JSON body with the content type the request declared', () => { + const headers = { 'content-type': 'application/json' }; + + expect(buildPostData('{"bio":"x"}', headers)).toEqual({ + mimeType: 'application/json', + text: '{"bio":"x"}', + }); + }); + + it('matches the content-type header regardless of its casing', () => { + expect(buildPostData('{}', { 'Content-Type': 'application/json' }).mimeType).toBe( + 'application/json' + ); + }); + + it('reads the content type from the flat array header form', () => { + const headers = ['accept', '*/*', 'content-type', 'application/json']; + + expect(buildPostData('{}', headers).mimeType).toBe('application/json'); + }); + + it('reads the content type from a Headers-like object', () => { + const headers = new Map([['content-type', 'application/json']]); + + expect(buildPostData('{}', headers).mimeType).toBe('application/json'); + }); + + it('falls back to application/octet-stream when no content type was declared', () => { + expect(buildPostData('raw', {}).mimeType).toBe('application/octet-stream'); + }); + + it('returns an empty object for a request with no body, as before', () => { + expect(buildPostData(undefined, {})).toEqual({}); + }); + + it('treats an empty string body as no body', () => { + expect(buildPostData('', {})).toEqual({}); + }); + + it('serializes a URLSearchParams body', () => { + const body = new URLSearchParams({ a: '1', b: '2' }); + + expect(buildPostData(body, {}).text).toBe('a=1&b=2'); + }); + + it('does not attempt to serialize a stream body', () => { + expect(buildPostData(Buffer.from('binary'), {})).toEqual({}); + }); + + it('omits a non-string body rather than recording "[object Object]"', () => { + expect(buildPostData({ bio: 'x' }, { 'content-type': 'application/json' })).toEqual({}); + }); + + it('omits a FormData body, which cannot be read without consuming it', () => { + expect(buildPostData(new FormData(), { 'content-type': 'multipart/form-data' })).toEqual({}); + }); +}); diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts new file mode 100644 index 0000000000..257326173b --- /dev/null +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -0,0 +1,35 @@ +import { buildHeaders } from './build-headers.js'; + +/** + * The HAR `postData` entry for a request body. + * + * Without this the capture records the request line and headers but not what + * was sent, so anything reading the HAR back — `drift`'s request-body + * validation, for one — silently has nothing to check. + */ +export function buildPostData( + body: unknown, + headers: any = {} +): { mimeType?: string; text?: string } { + const text = serializeBody(body); + if (text === undefined || text === '') return {}; + + // Via `buildHeaders`, so every shape a request can carry its headers in is + // handled the same way here as it is for the entry's `headers` list. + const contentType = buildHeaders(headers).find( + ({ name }) => String(name).toLowerCase() === 'content-type' + )?.value; + + return { + mimeType: typeof contentType === 'string' ? contentType : 'application/octet-stream', + text, + }; +} + +/** Only bodies that are already text; a stream or binary body is left out. */ +function serializeBody(body: unknown): string | undefined { + if (typeof body === 'string') return body; + if (body instanceof URLSearchParams) return body.toString(); + + return undefined; +} 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 08def49927..5dc6011ca7 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -12,6 +12,7 @@ import { URL } from 'url'; import { addHeaders } from './helpers/add-headers.js'; import { buildHeaders } from './helpers/build-headers.js'; +import { buildPostData } from './helpers/build-post-data.js'; import { buildRequestCookies } from './helpers/build-request-cookies.js'; import { buildResponseCookies } from './helpers/build-response-cookies.js'; import { getDuration } from './helpers/get-duration.js'; @@ -43,6 +44,7 @@ export const withHar: WithHar = function ( const startTime = process.hrtime(); const url = new URL(typeof input === 'string' ? input : input.url); + const postData = buildPostData(options.body, options.headers || {}); const entry = { _compressed: false, @@ -82,8 +84,8 @@ export const withHar: WithHar = function ( value, })), headersSize: -1, - bodySize: -1, - postData: {}, + bodySize: postData.text === undefined ? -1 : Buffer.byteLength(postData.text), + postData, httpVersion: 'HTTP/1.1', }, response: {}, From a927ba436acd5c10d80aa2889f7c9c4e29c460d8 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 14:39:17 +0300 Subject: [PATCH 2/9] Update packages/cli/src/commands/respect/har-logs/with-har.ts --- packages/cli/src/commands/respect/har-logs/with-har.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 5dc6011ca7..150e81506e 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -84,7 +84,10 @@ export const withHar: WithHar = function ( value, })), headersSize: -1, - bodySize: postData.text === undefined ? -1 : Buffer.byteLength(postData.text), + // -1 means "not available": a body was sent but not recorded (for example FormData). + bodySize: + postData.text !== undefined ? Buffer.byteLength(postData.text) : options.body ? -1 : 0, + ...(postData.text !== undefined && { postData }), postData, httpVersion: 'HTTP/1.1', }, From c47db75fc1bb5e8f2977883abbb50210c08b143f Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 14:44:51 +0300 Subject: [PATCH 3/9] Apply suggestion from @DmitryAnansky --- .../src/commands/respect/har-logs/helpers/build-post-data.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts index 257326173b..1da86c349b 100644 --- a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -19,7 +19,10 @@ export function buildPostData( const contentType = buildHeaders(headers).find( ({ name }) => String(name).toLowerCase() === 'content-type' )?.value; - +const fallbackMimeType = + body instanceof URLSearchParams + ? 'application/x-www-form-urlencoded;charset=UTF-8' + : 'application/octet-stream'; return { mimeType: typeof contentType === 'string' ? contentType : 'application/octet-stream', text, From 7f82b99260e42979bf78ba31334bd1a5aae84875 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 14:45:01 +0300 Subject: [PATCH 4/9] Apply suggestion from @DmitryAnansky --- .../src/commands/respect/har-logs/helpers/build-post-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts index 1da86c349b..92d7417aed 100644 --- a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -24,7 +24,7 @@ const fallbackMimeType = ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/octet-stream'; return { - mimeType: typeof contentType === 'string' ? contentType : 'application/octet-stream', + mimeType: typeof contentType === 'string' ? contentType : fallbackMimeType, text, }; } From eb52047df77a7c4ada2a1a457ab86dc82bb13951 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 15:01:46 +0300 Subject: [PATCH 5/9] Update packages/cli/src/commands/respect/har-logs/with-har.ts --- packages/cli/src/commands/respect/har-logs/with-har.ts | 1 - 1 file changed, 1 deletion(-) 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 150e81506e..9f3dbafa93 100644 --- a/packages/cli/src/commands/respect/har-logs/with-har.ts +++ b/packages/cli/src/commands/respect/har-logs/with-har.ts @@ -88,7 +88,6 @@ export const withHar: WithHar = function ( bodySize: postData.text !== undefined ? Buffer.byteLength(postData.text) : options.body ? -1 : 0, ...(postData.text !== undefined && { postData }), - postData, httpVersion: 'HTTP/1.1', }, response: {}, From e4c537693f9b888f662e53795e6d2842798703ce Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 15:04:11 +0300 Subject: [PATCH 6/9] Apply suggestion from @DmitryAnansky --- .../src/commands/respect/har-logs/helpers/build-post-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts index 92d7417aed..62a598528a 100644 --- a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -19,7 +19,7 @@ export function buildPostData( const contentType = buildHeaders(headers).find( ({ name }) => String(name).toLowerCase() === 'content-type' )?.value; -const fallbackMimeType = + const fallbackMimeType = body instanceof URLSearchParams ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/octet-stream'; From f4005f9de6af569f73ba4f9cdd58b0126bae9c64 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 15:25:58 +0300 Subject: [PATCH 7/9] Update packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts --- .../src/commands/respect/har-logs/helpers/build-post-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts index 62a598528a..e20694ae4e 100644 --- a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -16,7 +16,7 @@ export function buildPostData( // Via `buildHeaders`, so every shape a request can carry its headers in is // handled the same way here as it is for the entry's `headers` list. - const contentType = buildHeaders(headers).find( + const contentTypeValue = buildHeaders(headers).find( ({ name }) => String(name).toLowerCase() === 'content-type' )?.value; const fallbackMimeType = From 32ee4f1edb7d7c87e8f9a4e79b494ab67ce5a665 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Tue, 25 Aug 2026 15:26:10 +0300 Subject: [PATCH 8/9] Update packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts --- .../src/commands/respect/har-logs/helpers/build-post-data.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts index e20694ae4e..9adec809f9 100644 --- a/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts +++ b/packages/cli/src/commands/respect/har-logs/helpers/build-post-data.ts @@ -19,6 +19,9 @@ export function buildPostData( const contentTypeValue = buildHeaders(headers).find( ({ name }) => String(name).toLowerCase() === 'content-type' )?.value; + const contentType = Array.isArray(contentTypeValue) + ? contentTypeValue.join(',') + : contentTypeValue; const fallbackMimeType = body instanceof URLSearchParams ? 'application/x-www-form-urlencoded;charset=UTF-8' From d0c4d82351a35b9ec155bac050d4279886374df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81=C4=99kawa?= <164185257+JLekawa@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:38:27 +0200 Subject: [PATCH 9/9] Apply suggestion from @JLekawa --- .changeset/respect-har-post-data.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/respect-har-post-data.md b/.changeset/respect-har-post-data.md index fe3750b77d..c307b30789 100644 --- a/.changeset/respect-har-post-data.md +++ b/.changeset/respect-har-post-data.md @@ -2,4 +2,6 @@ '@redocly/cli': patch --- -Fixed `respect --har-output` recording an empty `postData` for every request. Request bodies are now written to the HAR, so a capture replayed through `drift` can have its request bodies validated instead of silently passing. +Fixed an issue where `respect --har-output` recorded an empty `postData` for every request. +Request bodies are written to the HAR. +Captures replayed through `drift` can have their request bodies validated instead of silently passing.