Skip to content

fix: prevent partial response-header match TypeError (#527)#3772

Open
manwithacat wants to merge 1 commit intobigskysoftware:devfrom
manwithacat:fix/partial-header-match-typeerror
Open

fix: prevent partial response-header match TypeError (#527)#3772
manwithacat wants to merge 1 commit intobigskysoftware:devfrom
manwithacat:fix/partial-header-match-typeerror

Conversation

@manwithacat
Copy link
Copy Markdown
Contributor

Fixes #527 — a four-year-old bug that @1cg (thread comment) explicitly invited a fix for.

The bug

hasHeader(xhr, regex) ran regex.test(xhr.getAllResponseHeaders()) — testing against the concatenated header string. When a response carried a header whose name contained an htmx header name as a substring — the issue reporter's example was X-HX-Trigger: foo — the regex /HX-Trigger:/i matched the substring and returned true, but xhr.getResponseHeader('HX-Trigger') returned null (because the actual header name is X-HX-Trigger). handleTriggerHeader then crashed at src/htmx.js:2068 on triggerBody.indexOf('{').

The fix

Refactor hasHeader to use getResponseHeader(name) !== null directly:

- function hasHeader(xhr, regexp) {
-   return regexp.test(xhr.getAllResponseHeaders())
- }
+ function hasHeader(xhr, name) {
+   return xhr.getResponseHeader(name) !== null
+ }

Every one of the 12 call sites already knew the exact header name (they used it in both the regex and the paired getResponseHeader call), so the update is mechanical:

- if (hasHeader(xhr, /HX-Trigger:/i)) {
+ if (hasHeader(xhr, 'HX-Trigger')) {

The substring-match class of bug is eliminated — hasHeader and getResponseHeader now use identical header lookup semantics.

Tests

New regression test in test/core/headers.js:

  • does not crash or fire trigger when X-HX-Trigger header is present without HX-Trigger — sends X-HX-Trigger: foo, asserts the foo listener is not fired. Before the fix this crashed with TypeError: Cannot read properties of null (reading 'indexOf').

Full suite: 847 passed, 0 failed, 3 skipped, 99.9% coverage. ESLint clean.

A note on @1cg's original concern

The thread mentions Chrome "issuing pretty gnarly looking error messages when you ask for a header that doesn't exist." That was the original reason for the regex approach. Modern Chrome/Firefox/Safari all silently return null from getResponseHeader() for missing headers with no console noise, so that justification no longer applies.

)

hasHeader() tested a regex against getAllResponseHeaders() output, which
false-positively matched any header containing an htmx header name as a
substring — e.g. X-HX-Trigger or HX-Trigger-User. After the false-positive,
xhr.getResponseHeader('HX-Trigger') returned null, and handleTriggerHeader
crashed on triggerBody.indexOf('{').

Refactors hasHeader(xhr, regex) → hasHeader(xhr, name), using
getResponseHeader(name) !== null as the check. This is the exact check
the 12 call sites need, eliminates the substring-match class of bug, and
removes a layer of indirection (no more regex construction or
getAllResponseHeaders() scan per check).

Adds a regression test with X-HX-Trigger: foo that previously crashed
inside handleTriggerHeader.

Fixes bigskysoftware#527. Addresses @1cg's suggestion in that thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant