From 8c6570245aa4568fbd5c04820a859290f209807f Mon Sep 17 00:00:00 2001 From: Aries Clark Date: Thu, 30 Jul 2026 12:05:18 -0400 Subject: [PATCH] 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: {},