Skip to content

Commit 5b68f57

Browse files
authored
Recognize HTTP/2 streaming (#193)
1 parent 650328e commit 5b68f57

2 files changed

Lines changed: 52 additions & 15 deletions

File tree

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: 20 additions & 14 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;
@@ -27,10 +36,10 @@ declare const htmx: typeof htmxType;
2736
(xhr as any)._chunkedLastLen = 0;
2837

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

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

3544
const mode = (xhr as any)._chunkedMode || "append";
3645
const full = (xhr.response as string) ?? "";
@@ -55,7 +64,6 @@ declare const htmx: typeof htmxType;
5564
response = extension.transformResponse(response, xhr, elt);
5665
});
5766

58-
const swapSpec = api.getSwapSpecification(elt);
5967
const settleInfo = api.makeSettleInfo(elt);
6068

6169
if (api.swap) {
@@ -85,9 +93,7 @@ declare const htmx: typeof htmxType;
8593
const mode = (xhr as any)._chunkedMode;
8694
if (mode !== "swap") return;
8795

88-
const is_chunked =
89-
xhr.getResponseHeader("Transfer-Encoding") === "chunked";
90-
if (!is_chunked) return;
96+
if (!isChunkedTransfer(xhr)) return;
9197

9298
detail.shouldSwap = false;
9399
}
@@ -134,13 +140,13 @@ interface SwapSpec {
134140
}
135141

136142
interface SwapOptions {
137-
select: string;
138-
selectOOB: string;
139-
eventInfo: Object;
140-
anchor: Element;
141-
contextElement: Element;
142-
afterSwapCallback: () => void;
143-
afterSettleCallback: () => void;
143+
select?: string;
144+
selectOOB?: string;
145+
eventInfo?: Object;
146+
anchor?: Element;
147+
contextElement?: Element;
148+
afterSwapCallback?: () => void;
149+
afterSettleCallback?: () => void;
144150
}
145151

146152
interface SettleInfo {

0 commit comments

Comments
 (0)