11import { HttpMethod } from 'simple-http-request-builder' ;
2- import { HttpPromise , unwrapHttpPromise } from '../promise/HttpPromise' ;
3- import { networkErrorCatcher } from './FetchClient' ;
42import {
5- genericError , HttpResponse , networkError , timeoutError ,
6- } from './HttpResponse' ;
3+ FetchResponseHandler ,
4+ genericError ,
5+ HttpPromise ,
6+ HttpResponse ,
7+ networkErrorCatcher ,
8+ toErrorResponsePromise ,
9+ unwrapHttpPromise ,
10+ } from 'simple-http-rest-client' ;
11+ import { Logger } from 'simple-logging-system' ;
712import {
813 MultipartHttpClient ,
914 MultipartHttpOptions ,
1015 MultipartHttpRequest ,
1116} from './MultipartHttpRequest' ;
1217
13- /**
14- * Handle multipart request using {@link XMLHttpRequest}
15- * @param multipartHttpRequest the request to be executed
16- */
18+ const logger : Logger = new Logger ( 'MultipartHttpClient' ) ;
19+
1720export const multipartHttpFetchClientExecutor : MultipartHttpClient < Promise < unknown > > = (
1821 multipartHttpRequest : MultipartHttpRequest < unknown > ,
19- ) : Promise < unknown > => {
20- const xhr : XMLHttpRequest = new XMLHttpRequest ( ) ;
21-
22- // Abort request after configured timeout time
23- const timeoutHandle : ReturnType < typeof setTimeout > = setTimeout (
24- ( ) => xhr . abort ( ) ,
25- multipartHttpRequest . optionValues . timeoutInMillis ,
26- ) ;
27-
28- // Return a promise that resolves when the request is complete
29- return new Promise < unknown > ( ( resolve : ( value : unknown ) => void ) => {
30- xhr . open ( multipartHttpRequest . method , multipartHttpRequest . buildUrl ( ) , true ) ;
31-
32- // Set credentials
33- xhr . withCredentials = multipartHttpRequest . optionValues . withCredentials ;
34-
35- // Set headers
36- if ( multipartHttpRequest . headersValue ) {
37- for ( const [ key , value ] of Object . entries ( multipartHttpRequest . headersValue ) ) {
38- xhr . setRequestHeader ( key , value ) ;
39- }
40- }
22+ ) : Promise < Response > => {
23+ const { boundary } : MultipartHttpOptions = multipartHttpRequest . optionValues ;
24+ const stream : ReadableStream < unknown > = new ReadableStream ( {
25+ start ( controller : ReadableStreamDefaultController < unknown > ) {
26+ const encoder : TextEncoder = new TextEncoder ( ) ;
27+ const entries = Array . from ( multipartHttpRequest . formData . entries ( ) ) ;
28+ const processNextEntry = ( index : number ) => {
29+ if ( index >= entries . length ) {
30+ // Finalize the stream with the closing boundary
31+ controller . enqueue ( encoder . encode ( `--${ boundary } --\r\n` ) ) ;
32+ controller . close ( ) ;
33+ return ;
34+ }
4135
42- // Handle response
43- xhr . onload = ( ) => {
44- if ( xhr . status >= 200 && xhr . status < 300 ) {
45- return resolve ( { response : JSON . parse ( xhr . response ) } ) ;
46- }
47- return resolve ( { error : JSON . parse ( xhr . response ) ?? genericError } ) ;
48- } ;
36+ const [ name , data ] = entries [ index ] ;
37+ const enqueueBoundaryAndHeaders = ( ) => {
38+ controller . enqueue ( encoder . encode ( `--${ boundary } \r\n` ) ) ;
39+ if ( data instanceof Blob ) {
40+ // Binary data
41+ const contentType = data . type || 'application/octet-stream' ;
42+ controller . enqueue ( encoder . encode (
43+ `Content-Disposition: form-data; name="${ name } "; filename="${ name } "\r\n`
44+ + `Content-Type: ${ contentType } \r\n\r\n` ,
45+ ) ) ;
46+ return data . stream ( ) . getReader ( ) ;
47+ }
48+ // Text data
49+ controller . enqueue ( encoder . encode (
50+ `Content-Disposition: form-data; name="${ name } "\r\n\r\n` ,
51+ ) ) ;
52+ controller . enqueue ( encoder . encode ( `${ data . toString ( ) } \r\n` ) ) ;
53+ return null ;
54+ } ;
4955
50- // Handle network errors
51- xhr . onerror = ( ) => resolve ( { error : networkError } ) ;
56+ const reader = enqueueBoundaryAndHeaders ( ) ;
5257
53- // Handle request timeout
54- xhr . ontimeout = ( ) => resolve ( { error : timeoutError } ) ;
58+ // Handle text directly, or start processing binary data
59+ if ( ! reader ) {
60+ processNextEntry ( index + 1 ) ; // Process the next entry
61+ return ;
62+ }
5563
56- // Handle progress
57- xhr . upload . onprogress = ( event : ProgressEvent ) => {
58- multipartHttpRequest . optionValues . onProgressCallback ( event ) ;
59- } ;
64+ // Process the binary data with recursion
65+ const pumpReader = ( ) => {
66+ reader . read ( ) . then ( ( { done, value } ) => {
67+ if ( done ) {
68+ // Move to the next entry
69+ controller . enqueue ( encoder . encode ( '\r\n' ) ) ;
70+ processNextEntry ( index + 1 ) ;
71+ } else {
72+ // Enqueue the current chunk
73+ controller . enqueue ( value ) ;
74+ pumpReader ( ) ; // Process the next chunk
75+ }
76+ } ) ;
77+ } ;
6078
61- xhr . upload . onerror = ( ) => resolve ( { error : genericError } ) ;
79+ pumpReader ( ) ;
80+ } ;
6281
63- // Send the request
64- xhr . send ( multipartHttpRequest . formData ) ;
65- } )
82+ processNextEntry ( 0 ) ; // Start processing the first entry
83+ } ,
84+ cancel : ( reason : unknown ) => {
85+ multipartHttpRequest . optionValues . timeoutAbortController . abort ( reason ) ;
86+ } ,
87+ } ) ;
88+ const timeoutHandle = setTimeout (
89+ ( ) => stream . cancel ( 'timeout' ) ,
90+ multipartHttpRequest . optionValues . timeoutInMillis ,
91+ ) ;
92+ return fetch (
93+ multipartHttpRequest . buildUrl ( ) ,
94+ {
95+ headers : {
96+ ...multipartHttpRequest . headersValue ,
97+ 'Content-Type' : `multipart/form-data; boundary=${ boundary } ` ,
98+ } ,
99+ method : HttpMethod . POST ,
100+ body : stream ,
101+ signal : multipartHttpRequest . optionValues . timeoutAbortController . signal ,
102+ duplex : 'half' ,
103+ } ,
104+ )
66105 . finally ( ( ) => clearTimeout ( timeoutHandle ) ) ;
67106} ;
68107
69- /**
70- * A {@link MultipartHttpFetchClient} that executes an {@link MultipartHttpRequest} that returns JSON responses.
71- * It uses {@link multipartHttpFetchClient} to executes the {@link MultipartHttpRequest}.
72- */
73108export const multipartHttpFetchClient = < T = void > (
74109 httpRequest : MultipartHttpRequest < unknown > ,
110+ ...handlers : FetchResponseHandler [ ]
75111) : Promise < HttpResponse < T > > => < Promise < HttpResponse < T > > > multipartHttpFetchClientExecutor ( httpRequest )
112+ . then ( ( response ) => {
113+ for ( const handler of handlers ) {
114+ try {
115+ const handlerResult = handler ( response ) ;
116+ if ( handlerResult !== undefined ) {
117+ return handlerResult ;
118+ }
119+ } catch ( error ) {
120+ logger . error ( 'Error executing handler' , { error } ) ;
121+ return toErrorResponsePromise ( genericError ) ;
122+ }
123+ }
124+ return { response } ;
125+ } )
76126 . catch ( networkErrorCatcher ) ;
77127
78128export type MultipartHttpFetchClient = < T > (
79129 multipartHttpRequest : MultipartHttpRequest < unknown > ,
80130) => Promise < HttpResponse < T > > ;
81131
82- /**
83- * Factory function to create fetch {@link MultipartHttpRequest}.
84- *
85- * @param baseUrl The base URL. It should not contain an ending slash. A valid base URL is: http://hostname/api
86- * @param method The HTTP method used for the request, see {@link HttpMethod}
87- * @param path The path of the endpoint to call, it should be composed with a leading slash
88- * and will be appended to the {@link MultipartHttpRequest#baseUrl}. A valid path is: /users
89- * @param multipartHttpClient The fetch client that uses {@link MultipartHttpRequest} and returns
90- * a `Promise<HttpResponse<T>>`
91- * @param options Optional options to configure the request
92- */
93132export function createMultipartHttpFetchRequest < T > (
94133 baseUrl : string ,
95- method : HttpMethod ,
96134 path : string ,
97135 multipartHttpClient : MultipartHttpFetchClient ,
98136 options ?: Partial < MultipartHttpOptions > ,
@@ -103,7 +141,6 @@ export function createMultipartHttpFetchRequest<T>(
103141 multipartHttpRequest ,
104142 ) ,
105143 baseUrl ,
106- method ,
107144 path ,
108145 options ,
109146 ) ;
0 commit comments