@@ -13,9 +13,10 @@ import {
13
13
setCapturedScopesOnSpan ,
14
14
} from '@sentry/core' ;
15
15
import { getClient } from '@sentry/opentelemetry' ;
16
- import type { IntegrationFn , SanitizedRequestData } from '@sentry/types' ;
16
+ import type { IntegrationFn , PolymorphicRequest , Request , SanitizedRequestData } from '@sentry/types' ;
17
17
18
- import { getSanitizedUrlString , parseUrl , stripUrlQueryAndFragment } from '@sentry/utils' ;
18
+ import { getSanitizedUrlString , logger , parseUrl , stripUrlQueryAndFragment } from '@sentry/utils' ;
19
+ import { DEBUG_BUILD } from '../debug-build' ;
19
20
import type { NodeClient } from '../sdk/client' ;
20
21
import { setIsolationScope } from '../sdk/scope' ;
21
22
import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module' ;
@@ -148,7 +149,27 @@ export const instrumentHttp = Object.assign(
148
149
const isolationScope = ( scopes . isolationScope || getIsolationScope ( ) ) . clone ( ) ;
149
150
const scope = scopes . scope || getCurrentScope ( ) ;
150
151
152
+ const headers = req . headers ;
153
+ const host = headers . host || '<no host>' ;
154
+ const protocol = req . socket && ( req . socket as { encrypted ?: boolean } ) . encrypted ? 'https' : 'http' ;
155
+ const originalUrl = req . url || '' ;
156
+ const absoluteUrl = originalUrl . startsWith ( protocol ) ? originalUrl : `${ protocol } ://${ host } ${ originalUrl } ` ;
157
+
158
+ // This is non-standard, but may be set on e.g. Next.js or Express requests
159
+ const cookies = ( req as PolymorphicRequest ) . cookies ;
160
+
161
+ const normalizedRequest : Request = {
162
+ url : absoluteUrl ,
163
+ method : req . method ,
164
+ query_string : extractQueryParams ( req ) ,
165
+ headers : headersToDict ( req . headers ) ,
166
+ cookies,
167
+ } ;
168
+
169
+ patchRequestToCaptureBody ( req , normalizedRequest ) ;
170
+
151
171
// Update the isolation scope, isolate this request
172
+ isolationScope . setSDKProcessingMetadata ( { normalizedRequest } ) ;
152
173
isolationScope . setSDKProcessingMetadata ( { request : req } ) ;
153
174
154
175
const client = getClient < NodeClient > ( ) ;
@@ -301,3 +322,128 @@ function isKnownPrefetchRequest(req: HTTPModuleRequestIncomingMessage): boolean
301
322
// Currently only handles Next.js prefetch requests but may check other frameworks in the future.
302
323
return req . headers [ 'next-router-prefetch' ] === '1' ;
303
324
}
325
+
326
+ /**
327
+ * This method patches the request object to capture the body.
328
+ * Instead of actually consuming the streamed body ourselves, which has potential side effects,
329
+ * we monkey patch `req.on('data')` to intercept the body chunks.
330
+ * This way, we only read the body if the user also consumes the body, ensuring we do not change any behavior in unexpected ways.
331
+ */
332
+ function patchRequestToCaptureBody ( req : HTTPModuleRequestIncomingMessage , normalizedRequest : Request ) : void {
333
+ const chunks : Buffer [ ] = [ ] ;
334
+
335
+ /**
336
+ * We need to keep track of the original callbacks, in order to be able to remove listeners again.
337
+ * Since `off` depends on having the exact same function reference passed in, we need to be able to map
338
+ * original listeners to our wrapped ones.
339
+ */
340
+ const callbackMap = new WeakMap ( ) ;
341
+
342
+ try {
343
+ // eslint-disable-next-line @typescript-eslint/unbound-method
344
+ req . on = new Proxy ( req . on , {
345
+ apply : ( target , thisArg , args : Parameters < typeof req . on > ) => {
346
+ const [ event , listener ] = args ;
347
+
348
+ if ( event === 'data' ) {
349
+ const callback = new Proxy ( listener , {
350
+ apply : ( target , thisArg , args : Parameters < typeof listener > ) => {
351
+ const chunk = args [ 0 ] ;
352
+ try {
353
+ chunks . push ( chunk ) ;
354
+ } catch {
355
+ // ignore errors here...
356
+ }
357
+ return Reflect . apply ( target , thisArg , args ) ;
358
+ } ,
359
+ } ) ;
360
+
361
+ callbackMap . set ( listener , callback ) ;
362
+
363
+ return Reflect . apply ( target , thisArg , [ event , callback ] ) ;
364
+ }
365
+
366
+ if ( event === 'end' ) {
367
+ const callback = new Proxy ( listener , {
368
+ apply : ( target , thisArg , args ) => {
369
+ const body = Buffer . concat ( chunks ) . toString ( 'utf-8' ) ;
370
+
371
+ // We mutate the passed in normalizedRequest and add the body to it
372
+ if ( body ) {
373
+ normalizedRequest . data = body ;
374
+ }
375
+
376
+ return Reflect . apply ( target , thisArg , args ) ;
377
+ } ,
378
+ } ) ;
379
+
380
+ callbackMap . set ( listener , callback ) ;
381
+
382
+ return Reflect . apply ( target , thisArg , [ event , callback ] ) ;
383
+ }
384
+
385
+ return Reflect . apply ( target , thisArg , args ) ;
386
+ } ,
387
+ } ) ;
388
+
389
+ // Ensure we also remove callbacks correctly
390
+ // eslint-disable-next-line @typescript-eslint/unbound-method
391
+ req . off = new Proxy ( req . off , {
392
+ apply : ( target , thisArg , args : Parameters < typeof req . off > ) => {
393
+ const [ , listener ] = args ;
394
+
395
+ const callback = callbackMap . get ( listener ) ;
396
+ if ( callback ) {
397
+ callbackMap . delete ( listener ) ;
398
+
399
+ const modifiedArgs = args . slice ( ) ;
400
+ modifiedArgs [ 1 ] = callback ;
401
+ return Reflect . apply ( target , thisArg , modifiedArgs ) ;
402
+ }
403
+
404
+ return Reflect . apply ( target , thisArg , args ) ;
405
+ } ,
406
+ } ) ;
407
+ } catch {
408
+ // ignore errors if we can't patch stuff
409
+ }
410
+ }
411
+
412
+ function extractQueryParams ( req : IncomingMessage ) : string | undefined {
413
+ // url (including path and query string):
414
+ let originalUrl = req . url || '' ;
415
+
416
+ if ( ! originalUrl ) {
417
+ return ;
418
+ }
419
+
420
+ // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
421
+ // hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use.
422
+ if ( originalUrl . startsWith ( '/' ) ) {
423
+ originalUrl = `http://dogs.are.great${ originalUrl } ` ;
424
+ }
425
+
426
+ try {
427
+ const queryParams = new URL ( originalUrl ) . search . slice ( 1 ) ;
428
+ return queryParams . length ? queryParams : undefined ;
429
+ } catch {
430
+ return undefined ;
431
+ }
432
+ }
433
+
434
+ function headersToDict ( reqHeaders : Record < string , string | string [ ] | undefined > ) : Record < string , string > {
435
+ const headers : Record < string , string > = { } ;
436
+
437
+ try {
438
+ Object . entries ( reqHeaders ) . forEach ( ( [ key , value ] ) => {
439
+ if ( typeof value === 'string' ) {
440
+ headers [ key ] = value ;
441
+ }
442
+ } ) ;
443
+ } catch ( e ) {
444
+ DEBUG_BUILD &&
445
+ logger . warn ( 'Sentry failed extracting headers from a request object. If you see this, please file an issue.' ) ;
446
+ }
447
+
448
+ return headers ;
449
+ }
0 commit comments