mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(web): accept empty chunked API POST requests
Handle untyped zero-byte POST streams at the shared API parser boundary. Keep request.body undefined and preserve existing typed parsers, 404 fallbacks, authentication and webview proxy stream handling. Reject nonempty untyped streams on the first byte, and cover parser boundaries, raw HTTP framing and real WebServer routing with regressions.
This commit is contained in:
1 parent
3e06f3c141
commit
b0487cc9af
4 files changed
+487
No files matched your search
@@ -0,0 +1,40 @@
|
||||
/** Accept empty, untyped API POSTs even when a reverse proxy frames them as chunked. */
|
||||
import { errorCodes, type FastifyInstance, type FastifyRequest } from 'fastify';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
export function installEmptyApiPostParser(app: FastifyInstance): void {
|
||||
// Existing JSON, text and multipart parsers take precedence. The webview
|
||||
// proxy installs its own parser in an encapsulated scope.
|
||||
app.addContentTypeParser('*', async (req: FastifyRequest, payload: Readable) => {
|
||||
// Fastify normally skips an unsupported parser on its 404 path. Preserve
|
||||
// that behavior, including the unread stream used by referer fallbacks.
|
||||
if (req.is404) return undefined;
|
||||
|
||||
if (
|
||||
req.method !== 'POST' ||
|
||||
!req.routeOptions.url?.startsWith('/api/') ||
|
||||
req.headers['content-type'] !== undefined ||
|
||||
(req.headers['content-length'] !== undefined && req.headers['content-length'] !== '0')
|
||||
) {
|
||||
throw new errorCodes.FST_ERR_CTP_INVALID_MEDIA_TYPE();
|
||||
}
|
||||
|
||||
// Wait for the terminating chunk to prove the body is empty. Reject on the
|
||||
// first byte instead of buffering an unsupported body. Leave the socket
|
||||
// alive so Fastify can send its 415 and close the connection on parser errors.
|
||||
try {
|
||||
for await (const chunk of payload.iterator({ destroyOnReturn: false })) {
|
||||
if (Buffer.byteLength(chunk) > 0) throw new errorCodes.FST_ERR_CTP_INVALID_MEDIA_TYPE();
|
||||
}
|
||||
} catch (error) {
|
||||
// Match Fastify's body reader: an interrupted request is a client error.
|
||||
const err = error as Error & { statusCode?: number };
|
||||
if (typeof err.statusCode !== 'number' || err.statusCode < 400) err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// No synthetic JSON or header rewriting: required-body validation still
|
||||
// sees the same undefined value as a direct Content-Length: 0 request.
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -194,6 +194,7 @@ import {
|
||||
import { isMultiUserMode } from '../config/multiuser.js';
|
||||
import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
|
||||
import { installRouteErrorHandler } from './route-error-handler.js';
|
||||
import { installEmptyApiPostParser } from './request-body-parser.js';
|
||||
import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy, type HostPolicy } from './network-auth-policy.js';
|
||||
import {
|
||||
registerPushRoutes,
|
||||
@@ -970,6 +971,9 @@ export class WebServer extends EventEmitter {
|
||||
this.userIpFailures = authState.userIpFailures;
|
||||
}
|
||||
|
||||
// Bodyless API POSTs may arrive as empty chunked streams through a proxy.
|
||||
installEmptyApiPostParser(this.app);
|
||||
|
||||
// WebSocket support (terminal I/O — low-latency bidirectional channel)
|
||||
await this.app.register(fastifyWebsocket);
|
||||
|
||||
|
||||
Reference in new issue
Block a user