Skip to content

Commit b4a96fd

Browse files
committed
Recognize HTTP/2 streaming
The extension could reviously only recognize HTTP/1 streaming. HTTP/2 (TLS) doesn't allow the Transfer Encoding = "cunked" response header. This commit makes the extension look for other signs of streaming.
1 parent 7d424a5 commit b4a96fd

File tree

2 files changed

+47
-9
lines changed

2 files changed

+47
-9
lines changed

index.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ describe("chunked-transfer extension", () => {
6565
const mockXhr = {
6666
getResponseHeader: (header: string) => {
6767
if (header === "Transfer-Encoding") return null;
68+
// Non-chunked responses have Content-Length
69+
if (header === "Content-Length") return "22";
6870
return null;
6971
},
7072
response: "<p>Normal response</p>",
@@ -87,6 +89,34 @@ describe("chunked-transfer extension", () => {
8789
expect(target.innerHTML).toBe("");
8890
});
8991

92+
test("processes HTTP/2 streaming responses (no Transfer-Encoding or Content-Length)", () => {
93+
const element = document.createElement("div");
94+
const mockXhr = {
95+
getResponseHeader: (header: string) => {
96+
// HTTP/2 streaming: no Transfer-Encoding, no Content-Length
97+
return null;
98+
},
99+
response: "<p>HTTP/2 streamed content</p>",
100+
onprogress: null as any,
101+
};
102+
103+
const event = {
104+
target: element,
105+
detail: { xhr: mockXhr },
106+
};
107+
108+
registeredExtension.onEvent("htmx:beforeRequest", event);
109+
110+
// Verify onprogress handler was set
111+
expect(mockXhr.onprogress).toBeDefined();
112+
113+
// Simulate progress event
114+
mockXhr.onprogress!();
115+
116+
// Verify content was swapped
117+
expect(target.innerHTML).toBe("<p>HTTP/2 streamed content</p>");
118+
});
119+
90120
test("ignores events other than beforeRequest", () => {
91121
const element = document.createElement("div");
92122
const mockXhr = {
@@ -576,7 +606,8 @@ describe("chunked-transfer extension", () => {
576606

577607
const mockXhr = {
578608
getResponseHeader: (header: string) => {
579-
// NOT chunked
609+
// NOT chunked - has Content-Length
610+
if (header === "Content-Length") return "15";
580611
return null;
581612
},
582613
response: "<p>Final</p>",

index.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ declare const htmx: typeof htmxType;
1212
(function () {
1313
let api: HtmxApi;
1414

15+
// Helper function to detect chunked transfer (HTTP/1.1) or streaming (HTTP/2)
16+
function isChunkedTransfer(xhr: XMLHttpRequest): boolean {
17+
const te = xhr.getResponseHeader("Transfer-Encoding");
18+
const cl = xhr.getResponseHeader("Content-Length");
19+
const isHttp1Chunked = te === "chunked";
20+
const isStreamingWithoutLength = !te && !cl; // typical HTTP/2 streaming
21+
return isHttp1Chunked || isStreamingWithoutLength;
22+
}
23+
1524
htmx.defineExtension("chunked-transfer", {
1625
init: function (apiRef: HtmxApi) {
1726
api = apiRef;
@@ -22,14 +31,15 @@ declare const htmx: typeof htmxType;
2231

2332
if (name === "htmx:beforeRequest") {
2433
const xhr = evt.detail.xhr as XMLHttpRequest;
25-
(xhr as any)._chunkedMode = elt.getAttribute("hx-chunked-mode") || "append";
34+
(xhr as any)._chunkedMode =
35+
elt.getAttribute("hx-chunked-mode") || "append";
2636
(xhr as any)._chunkedLastLen = 0;
2737

2838
xhr.onprogress = function () {
29-
const is_chunked =
30-
xhr.getResponseHeader("Transfer-Encoding") === "chunked";
39+
if (!isChunkedTransfer(xhr)) return;
3140

32-
if (!is_chunked) return;
41+
const swapSpec = api.getSwapSpecification(elt);
42+
if (swapSpec.swapStyle !== "innerHTML") return;
3343

3444
const mode = (xhr as any)._chunkedMode || "append";
3545
const full = (xhr.response as string) ?? "";
@@ -54,7 +64,6 @@ declare const htmx: typeof htmxType;
5464
response = extension.transformResponse(response, xhr, elt);
5565
});
5666

57-
const swapSpec = api.getSwapSpecification(elt);
5867
const settleInfo = api.makeSettleInfo(elt);
5968

6069
if (api.swap) {
@@ -82,9 +91,7 @@ declare const htmx: typeof htmxType;
8291
const mode = (xhr as any)._chunkedMode;
8392
if (mode !== "swap") return;
8493

85-
const is_chunked =
86-
xhr.getResponseHeader("Transfer-Encoding") === "chunked";
87-
if (!is_chunked) return;
94+
if (!isChunkedTransfer(xhr)) return;
8895

8996
detail.shouldSwap = false;
9097
}

0 commit comments

Comments
 (0)