Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 79 additions & 55 deletions src/treaty2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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
})
Comment on lines +591 to +599

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t key streaming semantics off Transfer-Encoding, baka~

Transfer-Encoding: chunked is transport framing, not an application-level streaming contract. This branch makes ordinary chunked application/json, multipart/form-data, and application/octet-stream responses bypass their normal parsers and go through streamResponse() instead. Then streamResponse() feeds each raw chunk into parseStringifiedValue() (see src/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
-                    if (
-                        response.headers.get('Transfer-Encoding') ===
-                            'chunked' &&
-                        contentType !== 'text/event-stream'
-                    ) {
-                        data = streamResponse(response, {
-                            parseDate: config.parseDate,
-                            sse: false
-                        })
-                    } else {
-                        switch (contentType) {
+                    switch (contentType) {
                             case 'text/event-stream':
                                 data = streamResponse(response, {
                                     parseDate: config.parseDate
                                 })
                                 break
@@
-                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/treaty2/index.ts` around lines 591 - 599, Current code gates streaming by
Transfer-Encoding header which is a transport detail; update the condition so
streamResponse(...) is only used when the Content-Type explicitly denotes a
streaming format (e.g. 'text/event-stream',
'application/ndjson'/'application/x-ndjson', 'application/stream+json', or other
agreed streaming media types) or when the caller explicitly opts in (e.g. a
config flag like config.expectStream or config.stream === true). Modify the
check around response.headers.get('Transfer-Encoding') and contentType in the
block that calls streamResponse to instead validate contentType against the
allowed streaming types or check the explicit config opt-in, leaving normal
content types (application/json, multipart/form-data, application/octet-stream)
to be processed by the regular parsers; keep use of streamResponse and
parseStringifiedValue unchanged but only reachable via the new explicit
criteria.

} 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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
fi

Repository: elysiajs/eden

Length of output: 1344


🌐 Web query:

Biome noSwitchDeclarations rule lint configuration

💡 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 case in braces, you stubborn dweeb~

The bare const temp declaration is gonna trigger Biome's noSwitchDeclarations rule and block your CI, yoouuu~ Just slap some braces around it and problem solved, easy peasyyy~ Then rerun Biome like a good dev (๑•́ ω •̀)و

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.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/treaty2/index.ts` around lines 628 - 638, The switch case handling
'multipart/form-data' declares const temp directly and triggers the
noSwitchDeclarations lint rule; wrap the entire case body in braces so the const
temp (result of await response.formData()) is block-scoped, then populate data
by iterating temp.forEach(...) as before (refer to the case
'multipart/form-data', temp, response.formData(), and data assignment). After
adding the braces, rerun Biome/CI.


default:
data = await response.text().then((text) =>
parseStringifiedValue(text, {
parseDate: config.parseDate
})
)
}
}

if (response.status >= 300 || response.status < 200) {
Expand Down