-
Notifications
You must be signed in to change notification settings - Fork 619
fix(instrumentation-undici): fix several header handling handling bugs #2781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,6 +165,49 @@ export class UndiciInstrumentation extends InstrumentationBase<UndiciInstrumenta | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * For each header in the request, call the callback. Skips likely-invalid | ||
| * headers. Multi-valued headers are passed through. The loop exits early if | ||
| * the callback returns true. | ||
| */ | ||
| private forEachRequestHeader( | ||
| request: UndiciRequest, | ||
| callback: (key: string, value: string | string[]) => boolean | undefined | ||
| ): void { | ||
| if (Array.isArray(request.headers)) { | ||
| // headers are an array [k1, v2, k2, v2] (undici v6+) | ||
| for (let i = 0; i < request.headers.length; i += 2) { | ||
| const key = request.headers[i]; | ||
| if (typeof key !== 'string') { | ||
| // Shouldn't happen, but the types don't know that, and let's be safe | ||
| continue; | ||
| } | ||
| if (callback(key, request.headers[i + 1])) { | ||
| break; | ||
| } | ||
| } | ||
| } else if (typeof request.headers === 'string') { | ||
| // headers are a raw string (undici v5) | ||
| const headers = request.headers.split('\r\n'); | ||
| for (const line of headers) { | ||
| if (!line) { | ||
| continue; | ||
| } | ||
| const colonIndex = line.indexOf(':'); | ||
| if (colonIndex === -1) { | ||
| // Invalid header? Probably this can't happen, but again let's be safe. | ||
| continue; | ||
| } | ||
| const key = line.substring(0, colonIndex); | ||
| const value = line.substring(0, colonIndex + 1); | ||
|
|
||
| if (callback(key, value)) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // This is the 1st message we receive for each request (fired after request creation). Here we will | ||
| // create the span and populate some atttributes, then link the span to the request for further | ||
| // span processing | ||
|
|
@@ -218,26 +261,16 @@ export class UndiciInstrumentation extends InstrumentationBase<UndiciInstrumenta | |
| } | ||
|
|
||
| // Get user agent from headers | ||
| let userAgent; | ||
| if (Array.isArray(request.headers)) { | ||
| const idx = request.headers.findIndex( | ||
| h => h.toLowerCase() === 'user-agent' | ||
| ); | ||
| if (idx >= 0) { | ||
| userAgent = request.headers[idx + 1]; | ||
| this.forEachRequestHeader(request, (key, value) => { | ||
| if (key.toLowerCase() === 'user-agent') { | ||
| // user-agent should only appear once per the spec, but the library doesn't | ||
| // prevent passing it multiple times, so we handle that to be safe. | ||
| const userAgent = Array.isArray(value) ? value[0] : value; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One may wonder if the 1st occurrence of the UA header value may be the best option but I guess there is no best option in this scenario. If the request not complies the spec and sets multiple values for UA it may be good to log this issue.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could log it, although I dunno where that goes in practice? We would probably also be spec-compliant-ish to do |
||
| attributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; | ||
| return true; // no need to keep iterating | ||
|
||
| } | ||
| } else if (typeof request.headers === 'string') { | ||
| const headers = request.headers.split('\r\n'); | ||
| const uaHeader = headers.find(h => | ||
| h.toLowerCase().startsWith('user-agent') | ||
| ); | ||
| userAgent = | ||
| uaHeader && uaHeader.substring(uaHeader.indexOf(':') + 1).trim(); | ||
| } | ||
|
|
||
| if (userAgent) { | ||
| attributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; | ||
| } | ||
| return false; | ||
| }); | ||
|
|
||
| // Get attributes from the hook if present | ||
| const hookAttributes = safeExecuteInTheMiddle( | ||
|
|
@@ -330,25 +363,14 @@ export class UndiciInstrumentation extends InstrumentationBase<UndiciInstrumenta | |
| config.headersToSpanAttributes.requestHeaders.map(n => n.toLowerCase()) | ||
| ); | ||
|
|
||
| // headers could be in form | ||
| // ['name: value', ...] for v5 | ||
| // ['name', 'value', ...] for v6 | ||
| const rawHeaders = Array.isArray(request.headers) | ||
| ? request.headers | ||
| : request.headers.split('\r\n'); | ||
| rawHeaders.forEach((h, idx) => { | ||
| const sepIndex = h.indexOf(':'); | ||
| const hasSeparator = sepIndex !== -1; | ||
| const name = ( | ||
| hasSeparator ? h.substring(0, sepIndex) : h | ||
| ).toLowerCase(); | ||
| const value = hasSeparator | ||
| ? h.substring(sepIndex + 1) | ||
| : rawHeaders[idx + 1]; | ||
|
|
||
| this.forEachRequestHeader(request, (key, value) => { | ||
| const name = key.toLowerCase(); | ||
| if (headersToAttribs.has(name)) { | ||
| spanAttributes[`http.request.header.${name}`] = value.trim(); | ||
| spanAttributes[`http.request.header.${name}`] = value | ||
| .toString() | ||
| .trim(); | ||
| } | ||
| return false; // keep iterating always, there may be more | ||
| }); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this capture the key again since you're starting from the zeroth index of the
linestring again? Could be something like:const value = line.substring(colonIndex + 1).trim();There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eek, you're right!
Which makes me think -- do we have a way to actually unit test the patches against older library versions? I guess that would have to be a separate test package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
test-all- versionsscript should perform these tests. You can inspect which versions are tested in the.tav.ymlfile at the root of this instrumentation.