-
-
Notifications
You must be signed in to change notification settings - Fork 75
fix: #255 handle Transfer-Encoding: chunked
#256
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -6,10 +6,7 @@ import type { Treaty } from './types' | |
|
|
||
| import { EdenFetchError } from '../errors' | ||
| import { EdenWS } from './ws' | ||
| import { | ||
| parseStringifiedDate, | ||
| parseStringifiedValue | ||
| } from '../utils/parse' | ||
| import { parseStringifiedDate, parseStringifiedValue } from '../utils/parse' | ||
| import type { ThrowHttpError } from '../types' | ||
|
|
||
| const method = [ | ||
|
|
@@ -171,8 +168,13 @@ function* extractEvents( | |
|
|
||
| export async function* streamResponse( | ||
| response: Response, | ||
| options?: { parseDate?: boolean } | ||
| options?: { parseDate?: boolean; sse?: boolean } | ||
| ) { | ||
| const sse = | ||
| typeof options?.sse === 'boolean' | ||
| ? options.sse | ||
| : response.headers.get('Content-Type')?.split(';')[0] === | ||
| 'text/event-stream' | ||
| const body = response.body | ||
|
|
||
| if (!body) return | ||
|
|
@@ -191,22 +193,27 @@ export async function* streamResponse( | |
| ? value | ||
| : decoder.decode(value, { stream: true }) | ||
|
|
||
| bufferRef.value += chunk | ||
|
|
||
| yield* extractEvents(bufferRef, options) | ||
| if (sse) { | ||
| bufferRef.value += chunk | ||
| yield* extractEvents(bufferRef, options) | ||
| } else { | ||
| yield parseStringifiedValue(chunk, options) | ||
| } | ||
| } | ||
|
|
||
| const remaining = decoder.decode() | ||
| if (remaining) { | ||
| bufferRef.value += remaining | ||
| } | ||
| if (sse) { | ||
| const remaining = decoder.decode() | ||
| if (remaining) { | ||
| bufferRef.value += remaining | ||
| } | ||
|
|
||
| yield* extractEvents(bufferRef, options) | ||
| yield* extractEvents(bufferRef, options) | ||
|
|
||
| if (bufferRef.value.trim()) { | ||
| const parsed = parseSSEBlock(bufferRef.value, options) | ||
| if (parsed) { | ||
| yield parsed | ||
| if (bufferRef.value.trim()) { | ||
| const parsed = parseSSEBlock(bufferRef.value, options) | ||
| if (parsed) { | ||
| yield parsed | ||
| } | ||
| } | ||
| } | ||
| } finally { | ||
|
|
@@ -264,11 +271,11 @@ const createProxy = ( | |
| const append = (key: string, value: unknown) => { | ||
| // Explicitly exclude null and undefined values from url encoding | ||
| // to prevent parsing string "null" / string "undefined" | ||
| if (value === undefined || value === null) return | ||
| if (value === undefined || value === null) return | ||
|
|
||
| if (value instanceof Date) value = value.toISOString() | ||
| if (value instanceof Date) value = value.toISOString() | ||
|
|
||
| q += | ||
| q += | ||
| (q ? '&' : '?') + | ||
| `${encodeURIComponent(key)}=${encodeURIComponent( | ||
| typeof value === 'object' | ||
|
|
@@ -517,8 +524,8 @@ const createProxy = ( | |
| } | ||
| } | ||
|
|
||
| if (options?.headers?.['content-type']) | ||
| // @ts-ignore | ||
| if (options?.headers?.['content-type']) | ||
| // @ts-ignore | ||
| fetchInit.headers['content-type'] = | ||
| options?.headers['content-type'] | ||
|
|
||
|
|
@@ -577,49 +584,66 @@ const createProxy = ( | |
| } | ||
| } | ||
|
|
||
| switch ( | ||
| response.headers.get('Content-Type')?.split(';')[0] | ||
| ) { | ||
| case 'text/event-stream': | ||
| data = streamResponse(response, { | ||
| parseDate: config.parseDate | ||
| }) | ||
| break | ||
| const contentType = response.headers | ||
| .get('Content-Type') | ||
| ?.split(';')[0] | ||
|
|
||
| case 'application/json': | ||
| data = JSON.parse(await response.text(), (k, v) => { | ||
| if (typeof v !== 'string') return v | ||
|
|
||
| const date = parseStringifiedDate(v, { | ||
| if ( | ||
| response.headers.get('Transfer-Encoding') === | ||
| 'chunked' && | ||
| contentType !== 'text/event-stream' | ||
| ) { | ||
| data = streamResponse(response, { | ||
| parseDate: config.parseDate, | ||
| sse: false | ||
| }) | ||
| } else { | ||
| switch (contentType) { | ||
| case 'text/event-stream': | ||
| data = streamResponse(response, { | ||
| parseDate: config.parseDate | ||
| }) | ||
| if (date) return date | ||
| break | ||
|
|
||
| return v | ||
| }) | ||
| break | ||
| case 'application/json': | ||
| data = JSON.parse( | ||
| await response.text(), | ||
| (k, v) => { | ||
| if (typeof v !== 'string') return v | ||
|
|
||
| case 'application/octet-stream': | ||
| data = await response.arrayBuffer() | ||
| break | ||
| const date = parseStringifiedDate(v, { | ||
| parseDate: config.parseDate | ||
| }) | ||
| if (date) return date | ||
|
|
||
| case 'multipart/form-data': | ||
| const temp = (await response.formData()) as FormData | ||
| return v | ||
| } | ||
| ) | ||
| break | ||
|
|
||
| data = {} | ||
| temp.forEach((value, key) => { | ||
| // @ts-ignore | ||
| data[key] = value | ||
| }) | ||
| case 'application/octet-stream': | ||
| data = await response.arrayBuffer() | ||
| break | ||
|
|
||
| break | ||
| case 'multipart/form-data': | ||
| const temp = | ||
| (await response.formData()) as FormData | ||
|
|
||
| default: | ||
| data = await response.text().then((text) => | ||
| parseStringifiedValue(text, { | ||
| parseDate: config.parseDate | ||
| data = {} | ||
| temp.forEach((value, key) => { | ||
| // @ts-ignore | ||
| data[key] = value | ||
| }) | ||
| ) | ||
|
|
||
| break | ||
|
Comment on lines
+628
to
+638
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. 🧩 Analysis chain🏁 Script executed: # First, let's check if the file exists and read the relevant lines
if [ -f "src/treaty2/index.ts" ]; then
wc -l src/treaty2/index.ts
echo "---"
sed -n '620,650p' src/treaty2/index.ts
else
echo "File not found, searching for it..."
find . -name "index.ts" -path "*/treaty2/*" 2>/dev/null
fiRepository: elysiajs/eden Length of output: 1344 🌐 Web query:
💡 Result: The Biome linter rule "noSwitchDeclarations" (in the "correctness" group) disallows lexical declarations (const, let, function, class) in switch clauses because they are accessible across the entire switch statement but only initialized if their clause is reached, leading to potential bugs. Configuration in biome.json: { "linter": { "rules": { "correctness": { "noSwitchDeclarations": "error" } } } } It suggests wrapping declarations in a block {} to scope them to the clause. The rule is fixable (auto-wraps in blocks) and sources from ESLint's no-case-declarations. Citations:
Wrap this The bare Tiny fix- case 'multipart/form-data':
- const temp =
- (await response.formData()) as FormData
-
- data = {}
- temp.forEach((value, key) => {
- // `@ts-ignore`
- data[key] = value
- })
-
- break
+ case 'multipart/form-data': {
+ const temp =
+ (await response.formData()) as FormData
+
+ data = {}
+ temp.forEach((value, key) => {
+ // `@ts-ignore`
+ data[key] = value
+ })
+
+ break
+ }🧰 Tools🪛 Biome (2.4.9)[error] 629-630: Other switch clauses can erroneously access this declaration. (lint/correctness/noSwitchDeclarations) 🤖 Prompt for AI Agents |
||
|
|
||
| default: | ||
| data = await response.text().then((text) => | ||
| parseStringifiedValue(text, { | ||
| parseDate: config.parseDate | ||
| }) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| if (response.status >= 300 || response.status < 200) { | ||
|
|
||
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.
Don’t key streaming semantics off
Transfer-Encoding, baka~Transfer-Encoding: chunkedis transport framing, not an application-level streaming contract. This branch makes ordinary chunkedapplication/json,multipart/form-data, andapplication/octet-streamresponses bypass their normal parsers and go throughstreamResponse()instead. ThenstreamResponse()feeds each raw chunk intoparseStringifiedValue()(seesrc/utils/parse.ts, Lines 55-76), which only makes sense for complete values, not arbitrary HTTP chunk boundaries. Result: callers can suddenly get an async iterator or mangled chunk fragments for perfectly normal responses. Gate this on an explicit streaming format or caller opt-in instead. (¬‿¬)♡Suggested fix
🤖 Prompt for AI Agents