• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

elbywan / wretch / 19718210023

26 Nov 2025 09:43PM UTC coverage: 97.889% (-0.1%) from 98.031%
19718210023

push

github

elbywan
:bug: Improve body size computation when tracking upload progress

should solve #284

323 of 339 branches covered (95.28%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 1 file covered. (77.78%)

1671 of 1698 relevant lines covered (98.41%)

62.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.8
/src/addons/progress.ts
1
import type { ConfiguredMiddleware, Wretch, WretchAddon, WretchResponseChain } from "../types.js"
3✔
2

3✔
3
export type ProgressCallback = (loaded: number, total: number) => void
3✔
4

3✔
5
export interface ProgressResolver {
3✔
6
  /**
3✔
7
   * Provides a way to register a callback to be invoked one or multiple times during the download.
3✔
8
   * The callback receives the current progress as two arguments, the number of bytes downloaded and the total number of bytes to download.
3✔
9
   *
3✔
10
   * _Under the hood: this method adds a middleware to the chain that will intercept the response and replace the body with a new one that will emit the progress event._
3✔
11
   *
3✔
12
   * ```js
3✔
13
   * import ProgressAddon from "wretch/addons/progress"
3✔
14
   *
3✔
15
   * wretch("some_url")
3✔
16
   *   .addon(ProgressAddon())
3✔
17
   *   .get()
3✔
18
   *   .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`))
3✔
19
   * ```
3✔
20
   *
3✔
21
   * @param onProgress - A callback function for download progress
3✔
22
   */
3✔
23
  progress: <T, C extends ProgressResolver, R>(
3✔
24
    this: C & WretchResponseChain<T, C, R>,
3✔
25
    onProgress: ProgressCallback
3✔
26
  ) => this
3✔
27
}
3✔
28

3✔
29
export interface ProgressAddon {
3✔
30
  /**
3✔
31
   * Provides a way to register a callback to be invoked one or multiple times during the upload.
3✔
32
   * The callback receives the current progress as two arguments, the number of bytes uploaded and the total number of bytes to upload.
3✔
33
   *
3✔
34
   * **Browser Limitations:**
3✔
35
   * - **Firefox**: Does not support request body streaming (request.body is not readable). Upload progress monitoring will not work.
3✔
36
   * - **Chrome/Chromium**: Requires HTTPS connections (HTTP/2). Will fail with `ERR_ALPN_NEGOTIATION_FAILED` on HTTP servers.
3✔
37
   * - **Node.js**: Full support for both HTTP and HTTPS.
3✔
38
   *
3✔
39
   * _Compatible with platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility) and supporting request body streaming._
3✔
40
   *
3✔
41
   * ```js
3✔
42
   * import ProgressAddon from "wretch/addons/progress"
3✔
43
   *
3✔
44
   * wretch("https://example.com/upload") // Note: HTTPS required for Chrome
3✔
45
   *   .addon(ProgressAddon())
3✔
46
   *   .onUpload((loaded, total) => console.log(`Upload: ${(loaded / total * 100).toFixed(0)}%`))
3✔
47
   *   .post(formData)
3✔
48
   *   .res()
3✔
49
   * ```
3✔
50
   *
3✔
51
   * @param onUpload - A callback that will be called one or multiple times with the number of bytes uploaded and the total number of bytes to upload.
3✔
52
   */
3✔
53
  onUpload<T extends ProgressAddon, C, R, E>(this: T & Wretch<T, C, R, E>, onUpload: (loaded: number, total: number) => void): this
3✔
54

3✔
55
  /**
3✔
56
   * Provides a way to register a callback to be invoked one or multiple times during the download.
3✔
57
   * The callback receives the current progress as two arguments, the number of bytes downloaded and the total number of bytes to download.
3✔
58
   *
3✔
59
   * _Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._
3✔
60
   *
3✔
61
   * ```js
3✔
62
   * import ProgressAddon from "wretch/addons/progress"
3✔
63
   *
3✔
64
   * wretch("some_url")
3✔
65
   *   .addon(ProgressAddon())
3✔
66
   *   .onDownload((loaded, total) => console.log(`Download: ${(loaded / total * 100).toFixed(0)}%`))
3✔
67
   *   .get()
3✔
68
   *   .res()
3✔
69
   * ```
3✔
70
   *
3✔
71
   * @param onDownload - A callback that will be called one or multiple times with the number of bytes downloaded and the total number of bytes to download.
3✔
72
   */
3✔
73
  onDownload<T extends ProgressAddon, C, R, E>(this: T & Wretch<T, C, R, E>, onDownload: (loaded: number, total: number) => void): this
3✔
74
}
3✔
75

3✔
76
function toStream<T extends Request | Response>(requestOrResponse: T, bodySize: number, callback: ProgressCallback | undefined): T {
21✔
77
  try {
21✔
78
    const contentLength = requestOrResponse.headers.get("content-length")
21✔
79
    let total = bodySize || (contentLength ? +contentLength : 0)
21✔
80
    let loaded = 0
21✔
81
    const transform = new TransformStream({
21✔
82
      start() {
21✔
83
        callback?.(loaded, total)
21✔
84
      },
21✔
85
      transform(chunk, controller) {
21✔
86
        loaded += chunk.length
3✔
87
        if (total < loaded) {
3✔
88
          total = loaded
3✔
89
        }
3✔
90
        callback?.(loaded, total)
3✔
91
        controller.enqueue(chunk)
3✔
92
      }
3✔
93
    })
21✔
94

21✔
95
    const stream = requestOrResponse.body.pipeThrough(transform)
21✔
96

21✔
97
    if("status" in requestOrResponse) {
21✔
98
      return new Response(stream, requestOrResponse) as T
3✔
99
    } else {
21!
100
      // @ts-expect-error RequestInit does not yet include duplex
×
101
      return new Request(requestOrResponse, { body: stream, duplex: "half" }) as T
×
102
    }
×
103
  } catch {
21✔
104
    return requestOrResponse
18✔
105
  }
18✔
106
}
21✔
107

3✔
108
/**
3✔
109
 * Adds the ability to monitor progress when downloading a response.
3✔
110
 *
3✔
111
 * _Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._
3✔
112
 *
3✔
113
 * ```js
3✔
114
 * import ProgressAddon from "wretch/addons/progress"
3✔
115
 *
3✔
116
 * wretch("some_url")
3✔
117
 *   // Register the addon
3✔
118
 *   .addon(ProgressAddon())
3✔
119
 *   .get()
3✔
120
 *   // Log the progress as a percentage of completion
3✔
121
 *   .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`))
3✔
122
 * ```
3✔
123
 */
3✔
124
const progress: () => WretchAddon<ProgressAddon, ProgressResolver> = () => {
3✔
125
  function downloadMiddleware(state: Record<any, any>) : ConfiguredMiddleware {
21✔
126
    return next => (url, opts) => {
21✔
127
      return next(url, opts).then(response => {
21✔
128
        if (!state.progress) {
21✔
129
          return response
15✔
130
        }
15✔
131
        return toStream(response, 0, state.progress)
21✔
132
      })
21✔
133
    }
21✔
134
  }
21✔
135

21✔
136
  function uploadMiddleware(state: Record<any, any>): ConfiguredMiddleware {
21✔
137
    return next => async (url, opts) => {
21✔
138
      const body = opts.body
21✔
139

21✔
140
      if (!body || !state.upload) {
21✔
141
        return next(url, opts)
6✔
142
      }
6✔
143

21✔
144
      let bodySize: number = 0
21✔
145

15✔
146
      try {
15✔
147
        bodySize = (await new Request("a:", { method: "POST", body }).blob()).size
15✔
148
      } catch {
21!
NEW
149
        // Unable to determine body size
×
NEW
150
      }
×
151

21✔
152
      const request = toStream(new Request(url, opts), bodySize, state.upload)
21✔
153
      return next(request.url, request)
15✔
154
    }
21✔
155
  }
21✔
156

21✔
157
  return {
21✔
158
    beforeRequest(wretch, options, state) {
21✔
159
      const middlewares = []
21✔
160
      if (options.__uploadProgressCallback) {
21✔
161
        state.upload = options.__uploadProgressCallback
18✔
162
        delete options.__uploadProgressCallback
18✔
163
      }
18✔
164
      if (options.__downloadProgressCallback) {
21✔
165
        state.progress = options.__downloadProgressCallback
3✔
166
        delete options.__downloadProgressCallback
3✔
167
      }
3✔
168
      middlewares.push(uploadMiddleware(state))
21✔
169
      middlewares.push(downloadMiddleware(state))
21✔
170
      return wretch.middlewares(middlewares)
21✔
171
    },
21✔
172
    wretch: {
21✔
173
      onUpload(onUpload: (loaded: number, total: number) => void) {
21✔
174
        return this.options({ __uploadProgressCallback: onUpload })
18✔
175
      },
18✔
176
      onDownload(onDownload: (loaded: number, total: number) => void) {
21✔
177
        return this.options({ __downloadProgressCallback: onDownload })
3✔
178
      }
3✔
179
    },
21✔
180
    resolver: {
21✔
181
      progress(onProgress: ProgressCallback) {
21✔
182
        this._sharedState.progress = onProgress
3✔
183
        return this
3✔
184
      }
3✔
185
    },
21✔
186
  }
21✔
187
}
21✔
188

3✔
189
export default progress
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc