Skip to content

Commit 20b8db5

Browse files
authored
Fix logging middleware body reading and register extra params globally (#53)
* Fix body reading in logging middleware for non-JSON responses The logging middleware was calling response.json() and falling back to response.text() on the same cloned response. This fails when json() fails to parse (e.g., HTML error pages from Cloudflare) because json() consumes the body stream even when parsing fails. Fix: Read as text() first, then try JSON.parse() on the string. This only consumes the body once and handles non-JSON responses correctly. * Register extra params middleware in global registry The extra params (--extra-query, --extra-body, --extra-headers) were being parsed but never applied to requests. This fix registers the extra params middleware in the global middleware registry during BaseCommand.init(), so it applies to ALL HTTP clients (WebDAV, OCAPI, SLAS, ODS, MRT, Custom APIs). * Fix extra params middleware for GET/HEAD requests - Skip adding body to GET/HEAD requests which don't allow request bodies - Clean up global middleware registry between tests to prevent leakage - Fix query params to use modifiedRequest instead of original request
1 parent 2b4e60a commit 20b8db5

3 files changed

Lines changed: 66 additions & 55 deletions

File tree

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
} from './hooks.js';
1515
import {setLanguage} from '../i18n/index.js';
1616
import {configureLogger, getLogger, type LogLevel, type Logger} from '../logging/index.js';
17-
import type {ExtraParamsConfig} from '../clients/middleware.js';
17+
import {createExtraParamsMiddleware, type ExtraParamsConfig} from '../clients/middleware.js';
1818
import type {ConfigSource} from '../config/types.js';
1919
import {globalMiddlewareRegistry} from '../clients/middleware-registry.js';
2020

@@ -122,6 +122,10 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
122122

123123
this.configureLogging();
124124

125+
// Register extra params middleware (from --extra-query, --extra-body, --extra-headers flags)
126+
// This must happen before any API clients are created
127+
this.registerExtraParamsMiddleware();
128+
125129
// Collect middleware from plugins before any API clients are created
126130
await this.collectPluginHttpMiddleware();
127131

@@ -357,4 +361,20 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
357361

358362
return config;
359363
}
364+
365+
/**
366+
* Register extra params (query, body, headers) as global middleware.
367+
* This applies to ALL HTTP clients created during command execution.
368+
*/
369+
private registerExtraParamsMiddleware(): void {
370+
const extraParams = this.getExtraParams();
371+
if (!extraParams) return;
372+
373+
globalMiddlewareRegistry.register({
374+
name: 'cli-extra-params',
375+
getMiddleware() {
376+
return createExtraParamsMiddleware(extraParams);
377+
},
378+
});
379+
}
360380
}

packages/b2c-tooling-sdk/src/clients/middleware.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ export function createLoggingMiddleware(config?: string | LoggingMiddlewareConfi
165165

166166
const clonedResponse = response.clone();
167167
let responseBody: unknown;
168+
// Read as text first, then try to parse as JSON.
169+
// This avoids a bug where json() consumes the body stream even when parsing fails,
170+
// making subsequent text() calls fail with "Body has already been read".
171+
const text = await clonedResponse.text();
168172
try {
169-
responseBody = await clonedResponse.json();
173+
responseBody = JSON.parse(text);
170174
} catch {
171-
responseBody = await clonedResponse.text();
175+
responseBody = text;
172176
}
173177

174178
// Mask sensitive/large body keys before logging
@@ -208,6 +212,10 @@ export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middlewa
208212
async onRequest({request}) {
209213
let modifiedRequest = request;
210214

215+
// HTTP methods that don't allow a request body
216+
const methodsWithoutBody = ['GET', 'HEAD'];
217+
const canHaveBody = !methodsWithoutBody.includes(modifiedRequest.method.toUpperCase());
218+
211219
// Add extra headers first (before other modifications)
212220
if (config.headers && Object.keys(config.headers).length > 0) {
213221
const newHeaders = new Headers(modifiedRequest.headers);
@@ -218,28 +226,26 @@ export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middlewa
218226
modifiedRequest = new Request(modifiedRequest.url, {
219227
method: modifiedRequest.method,
220228
headers: newHeaders,
221-
body: modifiedRequest.body,
222-
duplex: modifiedRequest.body ? 'half' : undefined,
229+
...(canHaveBody && modifiedRequest.body ? {body: modifiedRequest.body, duplex: 'half'} : {}),
223230
} as RequestInit);
224231
}
225232

226233
// Add extra query parameters
227234
if (config.query && Object.keys(config.query).length > 0) {
228-
const url = new URL(request.url);
235+
const url = new URL(modifiedRequest.url);
229236
for (const [key, value] of Object.entries(config.query)) {
230237
if (value !== undefined) {
231238
url.searchParams.set(key, String(value));
232239
}
233240
}
234241
logger.trace(
235-
{extraQuery: config.query, originalUrl: request.url, newUrl: url.toString()},
242+
{extraQuery: config.query, originalUrl: modifiedRequest.url, newUrl: url.toString()},
236243
'[ExtraParams] Adding extra query params to URL',
237244
);
238245
modifiedRequest = new Request(url.toString(), {
239-
method: request.method,
240-
headers: request.headers,
241-
body: request.body,
242-
duplex: request.body ? 'half' : undefined,
246+
method: modifiedRequest.method,
247+
headers: modifiedRequest.headers,
248+
...(canHaveBody && modifiedRequest.body ? {body: modifiedRequest.body, duplex: 'half'} : {}),
243249
} as RequestInit);
244250
}
245251

@@ -264,8 +270,8 @@ export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middlewa
264270
} catch {
265271
logger.warn('[ExtraParams] Could not parse request body as JSON, skipping body merge');
266272
}
267-
} else if (!modifiedRequest.body) {
268-
// No existing body, create one with extra fields
273+
} else if (!modifiedRequest.body && canHaveBody) {
274+
// No existing body, create one with extra fields (only for methods that allow a body)
269275
logger.trace({body: config.body}, '[ExtraParams] Creating new body with extra fields');
270276
const headers = new Headers(modifiedRequest.headers);
271277
headers.set('content-type', 'application/json');

packages/b2c-tooling-sdk/test/cli/base-command.test.ts

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {expect} from 'chai';
77
import {Config} from '@oclif/core';
88
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
9+
import {globalMiddlewareRegistry} from '@salesforce/b2c-tooling-sdk/clients';
910

1011
// Create a concrete test command class
1112
class TestBaseCommand extends BaseCommand<typeof TestBaseCommand> {
@@ -57,6 +58,11 @@ describe('cli/base-command', () => {
5758
command = new TestBaseCommand([], config);
5859
});
5960

61+
afterEach(() => {
62+
// Clean up the global middleware registry between tests
63+
globalMiddlewareRegistry.clear();
64+
});
65+
6066
describe('init', () => {
6167
it('initializes command with default flags', async () => {
6268
// Mock parse method
@@ -401,23 +407,16 @@ describe('cli/base-command', () => {
401407
metadata: {},
402408
})) as typeof cmd.parse;
403409

404-
await cmd.init();
405-
let errorCalled = false;
406-
const originalError = cmd.error.bind(command);
407-
cmd.error = () => {
408-
errorCalled = true;
409-
throw new Error('Expected error');
410-
};
411-
410+
// Error is thrown during init() when registerExtraParamsMiddleware() calls getExtraParams()
411+
let errorThrown = false;
412412
try {
413-
command.testGetExtraParams();
414-
} catch {
415-
// Expected
413+
await cmd.init();
414+
} catch (err) {
415+
errorThrown = true;
416+
expect((err as Error).message).to.include('Invalid JSON for --extra-query');
416417
}
417418

418-
expect(errorCalled).to.be.true;
419-
420-
cmd.error = originalError;
419+
expect(errorThrown).to.be.true;
421420
cmd.parse = originalParse;
422421
});
423422

@@ -430,23 +429,16 @@ describe('cli/base-command', () => {
430429
metadata: {},
431430
})) as typeof cmd.parse;
432431

433-
await cmd.init();
434-
let errorCalled = false;
435-
const originalError = cmd.error.bind(command);
436-
cmd.error = () => {
437-
errorCalled = true;
438-
throw new Error('Expected error');
439-
};
440-
432+
// Error is thrown during init() when registerExtraParamsMiddleware() calls getExtraParams()
433+
let errorThrown = false;
441434
try {
442-
command.testGetExtraParams();
443-
} catch {
444-
// Expected
435+
await cmd.init();
436+
} catch (err) {
437+
errorThrown = true;
438+
expect((err as Error).message).to.include('Invalid JSON for --extra-body');
445439
}
446440

447-
expect(errorCalled).to.be.true;
448-
449-
cmd.error = originalError;
441+
expect(errorThrown).to.be.true;
450442
cmd.parse = originalParse;
451443
});
452444

@@ -497,23 +489,16 @@ describe('cli/base-command', () => {
497489
metadata: {},
498490
})) as typeof cmd.parse;
499491

500-
await cmd.init();
501-
let errorCalled = false;
502-
const originalError = cmd.error.bind(command);
503-
cmd.error = () => {
504-
errorCalled = true;
505-
throw new Error('Expected error');
506-
};
507-
492+
// Error is thrown during init() when registerExtraParamsMiddleware() calls getExtraParams()
493+
let errorThrown = false;
508494
try {
509-
command.testGetExtraParams();
510-
} catch {
511-
// Expected
495+
await cmd.init();
496+
} catch (err) {
497+
errorThrown = true;
498+
expect((err as Error).message).to.include('Invalid JSON for --extra-headers');
512499
}
513500

514-
expect(errorCalled).to.be.true;
515-
516-
cmd.error = originalError;
501+
expect(errorThrown).to.be.true;
517502
cmd.parse = originalParse;
518503
});
519504
});

0 commit comments

Comments
 (0)