@@ -10,11 +10,30 @@ import { z } from 'zod';
1010import { AuthService } from '../auth/authService' ;
1111import { ServerConfig } from '../config/serverConfig' ;
1212import { ROOT_LOGGER } from '../logger' ;
13+ import { getRequestClientInfo } from './clientInfo' ;
1314import { CorsConfiguration } from './cors' ;
1415import { FrontendSsrRenderer } from './frontendSsr' ;
15- import { ApiQueryService } from './rest/apiQueryService' ;
16+ import { ApiQueryService , ApiRequestError } from './rest/apiQueryService' ;
1617import { ApiRouter } from './rest/createApiRouter' ;
1718
19+ type HttpErrorContext = {
20+ err ?: Error ;
21+ issues ?: z . ZodIssue [ ] ;
22+ responseBody ?: unknown ;
23+ } ;
24+
25+ function getHttpErrorContext ( response : express . Response ) : HttpErrorContext | null {
26+ return ( response . locals as { httpErrorContext ?: HttpErrorContext } ) . httpErrorContext ?? null ;
27+ }
28+
29+ function setHttpErrorContext ( response : express . Response , context : Partial < HttpErrorContext > ) : void {
30+ const locals = response . locals as { httpErrorContext ?: HttpErrorContext } ;
31+ locals . httpErrorContext = {
32+ ...locals . httpErrorContext ,
33+ ...context ,
34+ } ;
35+ }
36+
1837@injectable ( )
1938export class HttpApplication {
2039 readonly app : express . Application ;
@@ -47,42 +66,86 @@ export class HttpApplication {
4766 app . use ( ( req , res , next ) => {
4867 const requestId = randomUUID ( ) ;
4968 const startedAt = process . hrtime . bigint ( ) ;
69+ const client = getRequestClientInfo ( req ) ;
70+ const originalJson = res . json . bind ( res ) ;
5071 const requestLogger = logger . child ( {
5172 requestId,
5273 method : req . method ,
5374 path : req . originalUrl ,
5475 remoteAddress : req . ip ,
76+ deviceId : client . deviceId ,
77+ openReplaySessionId : client . openReplaySessionId ,
5578 } ) ;
5679
80+ res . json = ( ( body : unknown ) => {
81+ if ( res . statusCode >= 400 ) {
82+ setHttpErrorContext ( res , { responseBody : body } ) ;
83+ }
84+
85+ return originalJson ( body ) ;
86+ } ) as typeof res . json ;
87+
5788 res . on ( `finish` , ( ) => {
5889 const durationMs = Number ( process . hrtime . bigint ( ) - startedAt ) / 1_000_000 ;
59- requestLogger . trace ( {
60- event : `http.request.completed` ,
90+ const event = res . statusCode >= 400 ? `http.request.failed` : `http.request.completed` ;
91+ const logContext = {
92+ event,
6193 statusCode : res . statusCode ,
6294 durationMs : Number ( durationMs . toFixed ( 3 ) ) ,
6395 contentLength : res . getHeader ( `content-length` ) ?? null ,
6496 userAgent : req . get ( `user-agent` ) ?? null ,
65- } , `HTTP request completed` ) ;
97+ ...getHttpErrorContext ( res ) ,
98+ } ;
99+
100+ if ( res . statusCode >= 500 ) {
101+ requestLogger . error ( logContext , `HTTP request failed` ) ;
102+ return ;
103+ }
104+
105+ if ( res . statusCode >= 400 ) {
106+ requestLogger . warn ( logContext , `HTTP request failed` ) ;
107+ return ;
108+ }
109+
110+ requestLogger . trace ( logContext , `HTTP request completed` ) ;
66111 } ) ;
67112
68113 next ( ) ;
69114 } ) ;
70115
71116 app . use ( `/auth` , express . urlencoded ( { extended : false } ) , express . json ( ) , authService . handler ) ;
72117 app . use ( `/api` , apiRouter . router ) ;
73- app . use ( ( error : unknown , req : express . Request , res : express . Response , next : express . NextFunction ) => {
118+
119+ if ( existsSync ( this . frontendDistPath ) ) {
120+ app . use ( express . static ( this . frontendDistPath , { index : false } ) ) ;
121+ app . get ( / ^ (? ! \/ a p i (?: \/ | $ ) | \/ s o c k e t \. i o (?: \/ | $ ) ) .* / , async ( req , res ) => {
122+ const joinRedirectUrl = this . resolveJoinRedirectUrl ( req ) ;
123+ if ( joinRedirectUrl ) {
124+ res . redirect ( 302 , joinRedirectUrl ) ;
125+ return ;
126+ }
127+
128+ const archiveRedirectUrl = this . resolveArchiveRedirectUrl ( req ) ;
129+ if ( archiveRedirectUrl ) {
130+ res . redirect ( 302 , archiveRedirectUrl ) ;
131+ return ;
132+ }
133+
134+ const html = await this . frontendSsrRenderer . render ( req ) ;
135+ res . type ( `html` ) . send ( html ) ;
136+ } ) ;
137+ }
138+
139+ app . use ( ( error : unknown , _req : express . Request , res : express . Response , next : express . NextFunction ) => {
74140 if ( ! ( error instanceof z . ZodError ) ) {
75141 next ( error ) ;
76142 return ;
77143 }
78144
79- logger . warn ( {
145+ setHttpErrorContext ( res , {
80146 err : error ,
81- event : `http.request.invalid` ,
82- method : req . method ,
83- path : req . originalUrl ,
84147 issues : error . issues ,
85- } , `HTTP request validation failed` ) ;
148+ } ) ;
86149
87150 const friendlyMessage = error . issues
88151 . map ( ( issue ) => {
@@ -96,26 +159,22 @@ export class HttpApplication {
96159 issues : error . issues ,
97160 } ) ;
98161 } ) ;
162+ app . use ( ( error : unknown , _req : express . Request , res : express . Response , next : express . NextFunction ) => {
163+ if ( res . headersSent ) {
164+ next ( error ) ;
165+ return ;
166+ }
99167
100- if ( existsSync ( this . frontendDistPath ) ) {
101- app . use ( express . static ( this . frontendDistPath , { index : false } ) ) ;
102- app . get ( / ^ (? ! \/ a p i (?: \/ | $ ) | \/ s o c k e t \. i o (?: \/ | $ ) ) .* / , async ( req , res ) => {
103- const joinRedirectUrl = this . resolveJoinRedirectUrl ( req ) ;
104- if ( joinRedirectUrl ) {
105- res . redirect ( 302 , joinRedirectUrl ) ;
106- return ;
107- }
108-
109- const archiveRedirectUrl = this . resolveArchiveRedirectUrl ( req ) ;
110- if ( archiveRedirectUrl ) {
111- res . redirect ( 302 , archiveRedirectUrl ) ;
112- return ;
113- }
168+ if ( error instanceof ApiRequestError ) {
169+ setHttpErrorContext ( res , { err : error } ) ;
170+ res . status ( error . statusCode ) . json ( { error : error . message } ) ;
171+ return ;
172+ }
114173
115- const html = await this . frontendSsrRenderer . render ( req ) ;
116- res . type ( `html` ) . send ( html ) ;
117- } ) ;
118- }
174+ const normalizedError = error instanceof Error ? error : new Error ( `Unexpected server error` ) ;
175+ setHttpErrorContext ( res , { err : normalizedError } ) ;
176+ res . status ( 500 ) . json ( { error : `Internal server error.` } ) ;
177+ } ) ;
119178
120179 this . app = app ;
121180 }
0 commit comments