unjs / ofetch

😱 A better fetch API. Works on node, browser and workers.
MIT License
3.62k stars 119 forks source link

unexpected behavior from `ofetch.raw` and `onResponse` #405

Open o-az opened 2 weeks ago

o-az commented 2 weeks ago

Describe the change

I'm trying to track the response progress, something which I do all the time when using the native Fetch API. However I'm encountering unexpected behavior with ofetch:

import { ofetch } from "ofetch"

// native fetch: logs progress
fetch("http://localhost:3000")
  .then(response =>
    onResponse({
      response,
      onProgress: progress => console.info(progress)
    })
  )
  .catch(console.error)

// Does not log progress
ofetch
  .raw("http://localhost:3000")
  .then(response =>
    onResponse({
      response, // also tried passing `response._data` for body
      onProgress: progress => console.info(progress)
    })
  )
  .catch(console.error)

// Does not log progress
ofetch("http://localhost:3000")
  .then(response =>
    onResponse({
      response,
      onProgress: progress => console.info(progress)
    })
  )
  .catch(console.error)

// Does not log progress
ofetch("http://localhost:3000", {
  onResponse: async context =>
    onResponse({
      response: context.response,
      onProgress: async progress => console.info(progress)
    })
}).catch(console.error)

type MaybePromise<TValue> = TValue | Promise<TValue>

async function onResponse({
  response,
  onProgress
}: { response: Response; onProgress: (progress: number) => MaybePromise<void> }) {
  console.info(`Response status: ${response.status}`)

  const reader = response.body?.getReader()
  if (!reader) return

  const contentLengthHeader = response.headers.get("content-length")
  const length = contentLengthHeader ? +Number.parseInt(contentLengthHeader) : undefined

  let receivedLength = 0
  let chunks: Array<Uint8Array> = []

  while (true) {
    console.info("Reading...")

    const read = await reader?.read()
    if (read?.done || !read?.value) break

    receivedLength += read.value.length
    chunks.push(read.value)

    // Calculate progress as a percentage if length is defined
    const progress = length ? (receivedLength / length) * 100 : receivedLength
    await onProgress(progress)

    console.info(`Received ${receivedLength} of ${length} bytes`)
  }
}

testing server:

import http from "node:http"

http
  .createServer(async (req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain", "Content-Length": "110" })
    const chunks = ["Hello ", "World", "\n"]
    for (const chunk of chunks) {
      res.write(chunk)
      await new Promise(resolve => setTimeout(resolve, 1_000)) // Simulate delay between chunks
    }
    res.end()
  })
  .listen(3_000, "localhost", () => console.log("Server running at http://localhost:3000/"))

URLs

No response

Additional information

Aareksio commented 2 weeks ago

This is by design.


ofetch.raw is not the equivalent of native fetch (you can access it using ofetch.native). It only gives you the whole response object after the request has been completed, instead of only the data (.json() / .text() return value). I believe you could set the responseType parameter to 'stream' and it may skip awaiting data, see source. However, if you do so, you'll be responsible for parsing the response (eg. r.text()) yourself.

Also see: https://github.com/unjs/ofetch/issues/45