Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion packages/atxp-express/src/atxpExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,11 @@
* Old clients: see JSON-RPC error with code -30402 → Branch 1 matches
* New clients: see JSON-RPC error with code -30402 + full error.data → x402/mpp works
*/
function installPaymentResponseRewriter(res: Response, logger: import("@atxp/common").Logger): void {
/** @internal Exported for testing only. */
export function installPaymentResponseRewriter(res: Response, logger: import("@atxp/common").Logger): void {
const origEnd = res.end;
const origWrite = res.write;
const origWriteHead = res.writeHead;

// Rewrite helper shared by both res.write and res.end hooks.
// tryRewritePaymentResponse handles both SSE (data: lines) and plain JSON.
Expand All @@ -183,21 +185,66 @@
return tryRewritePaymentResponse(body, challenge, logger) ?? chunk;
}

// Defer writeHead until res.end so we can update Content-Length after
// rewriting the body. @hono/node-server's responseViaCache sets
// Content-Length from the original (pre-rewrite) body size, then calls
// writeHead before end. Without deferring, the client receives the
// original Content-Length but the rewritten (larger) body, causing
// JSON truncation.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let deferredWriteHead: any[] | null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
res.writeHead = function writeHeadDeferred(this: Response, ...args: any[]): any {
deferredWriteHead = args;
return this;
} as any;

Check warning on line 200 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

function flushWriteHead(self: Response): void {
if (!deferredWriteHead) return;
(origWriteHead as any).apply(self, deferredWriteHead);

Check warning on line 204 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
deferredWriteHead = null;
}

// Hook res.write for SSE streaming responses.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
res.write = function writeWithPaymentRewrite(this: Response, ...args: any[]): any {
flushWriteHead(this);
args[0] = rewriteChunk(args[0]);
return (origWrite as any).apply(this, args);

Check warning on line 213 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} as any;

Check warning on line 214 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

// Hook res.end for non-SSE (enableJsonResponse) responses.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
res.end = function endWithPaymentRewrite(this: Response, ...args: any[]): any {
res.end = origEnd;
res.write = origWrite;
res.writeHead = origWriteHead;
args[0] = rewriteChunk(args[0]);

// Update Content-Length in deferred writeHead to match the rewritten body.
if (deferredWriteHead) {
const newBody = args[0];
if (newBody != null) {
const newLength = typeof newBody === 'string'
? Buffer.byteLength(newBody)
: Buffer.isBuffer(newBody)
? newBody.length
: undefined;
if (newLength !== undefined) {
// writeHead(statusCode, headers) or writeHead(statusCode, statusMessage, headers)
const headersIdx = typeof deferredWriteHead[1] === 'string' ? 2 : 1;
const headers = deferredWriteHead[headersIdx];
if (headers && typeof headers === 'object') {
headers['Content-Length'] = newLength;
}
}
}
(origWriteHead as any).apply(this, deferredWriteHead);

Check warning on line 242 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
deferredWriteHead = null;
}

return (origEnd as any).apply(this, args);

Check warning on line 246 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} as any;

Check warning on line 247 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
}

/**
Expand Down
Loading
Loading