-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add MultipartHttpClient to handle file upload
- Loading branch information
Vincent Dubois
committed
Jan 10, 2025
1 parent
96e8337
commit 1dbbe13
Showing
3 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { HttpMethod } from 'simple-http-request-builder'; | ||
import { HttpPromise, unwrapHttpPromise } from '../promise/HttpPromise'; | ||
import { networkErrorCatcher } from './FetchClient'; | ||
import { | ||
genericError, HttpResponse, networkError, timeoutError, | ||
} from './HttpResponse'; | ||
import { | ||
MultipartHttpClient, | ||
MultipartHttpOptions, | ||
MultipartHttpRequest, | ||
} from './MultipartHttpRequest'; | ||
|
||
/** | ||
* Handle multipart request using {@link XMLHttpRequest} | ||
* @param multipartHttpRequest the request to be executed | ||
*/ | ||
export const multipartHttpFetchClientExecutor: MultipartHttpClient<Promise<unknown>> = ( | ||
multipartHttpRequest: MultipartHttpRequest<unknown>, | ||
): Promise<unknown> => { | ||
const xhr: XMLHttpRequest = new XMLHttpRequest(); | ||
|
||
// Abort request after configured timeout time | ||
const timeoutHandle: ReturnType<typeof setTimeout> = setTimeout( | ||
() => xhr.abort(), | ||
multipartHttpRequest.optionValues.timeoutInMillis, | ||
); | ||
|
||
// Return a promise that resolves when the request is complete | ||
return new Promise<unknown>((resolve: (value: unknown) => void) => { | ||
xhr.open(multipartHttpRequest.method, multipartHttpRequest.buildUrl(), true); | ||
|
||
// Set credentials | ||
xhr.withCredentials = multipartHttpRequest.optionValues.withCredentials; | ||
|
||
// Set headers | ||
if (multipartHttpRequest.headersValue) { | ||
for (const [key, value] of Object.entries(multipartHttpRequest.headersValue)) { | ||
xhr.setRequestHeader(key, value); | ||
} | ||
} | ||
|
||
// Handle response | ||
xhr.onload = () => { | ||
if (xhr.status >= 200 && xhr.status < 300) { | ||
return resolve({ response: xhr.response }); | ||
} | ||
return resolve({ error: xhr.response ?? genericError }); | ||
}; | ||
|
||
// Handle network errors | ||
xhr.onerror = () => resolve({ error: networkError }); | ||
|
||
// Handle request timeout | ||
xhr.ontimeout = () => resolve({ error: timeoutError }); | ||
|
||
// Handle progress | ||
xhr.upload.onprogress = (event: ProgressEvent) => { | ||
multipartHttpRequest.optionValues.onProgressCallback(event); | ||
}; | ||
|
||
xhr.upload.onerror = () => resolve({ error: genericError }); | ||
|
||
// Send the request | ||
xhr.send(multipartHttpRequest.formData); | ||
}) | ||
.finally(() => clearTimeout(timeoutHandle)); | ||
}; | ||
|
||
/** | ||
* A {@link MultipartHttpFetchClient} that executes an {@link MultipartHttpRequest} that returns JSON responses. | ||
* It uses {@link multipartHttpFetchClient} to executes the {@link MultipartHttpRequest}. | ||
*/ | ||
export const multipartHttpFetchClient = <T = void>( | ||
httpRequest: MultipartHttpRequest<unknown>, | ||
): Promise<HttpResponse<T>> => <Promise<HttpResponse<T>>>multipartHttpFetchClientExecutor(httpRequest) | ||
.catch(networkErrorCatcher); | ||
|
||
export type MultipartHttpFetchClient = <T>( | ||
multipartHttpRequest: MultipartHttpRequest<unknown>, | ||
) => Promise<HttpResponse<T>>; | ||
|
||
/** | ||
* Factory function to create fetch {@link MultipartHttpRequest}. | ||
* | ||
* @param baseUrl The base URL. It should not contain an ending slash. A valid base URL is: http://hostname/api | ||
* @param method The HTTP method used for the request, see {@link HttpMethod} | ||
* @param path The path of the endpoint to call, it should be composed with a leading slash | ||
* and will be appended to the {@link MultipartHttpRequest#baseUrl}. A valid path is: /users | ||
* @param multipartHttpClient The fetch client that uses {@link MultipartHttpRequest} and returns | ||
* a `Promise<HttpResponse<T>>` | ||
* @param options Optional options to configure the request | ||
*/ | ||
export function createMultipartHttpFetchRequest<T>( | ||
baseUrl: string, | ||
method: HttpMethod, | ||
path: string, | ||
multipartHttpClient: MultipartHttpFetchClient, | ||
options?: Partial<MultipartHttpOptions>, | ||
): MultipartHttpRequest<HttpPromise<T>> { | ||
return new MultipartHttpRequest<HttpPromise<T>>( | ||
(multipartHttpRequest: MultipartHttpRequest<unknown>) => new HttpPromise<T>( | ||
unwrapHttpPromise<T>(multipartHttpClient(multipartHttpRequest)), | ||
multipartHttpRequest, | ||
), | ||
baseUrl, | ||
method, | ||
path, | ||
options, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { HttpMethod } from 'simple-http-request-builder'; | ||
|
||
export type MultipartHttpOptions = { | ||
timeoutInMillis: number, | ||
onProgressCallback: (event: ProgressEvent) => void, | ||
withCredentials: boolean, | ||
}; | ||
|
||
export type MultipartHttpClient<T> = (request: MultipartHttpRequest<unknown>) => T; | ||
|
||
export class MultipartHttpRequest<T> { | ||
private static readonly DEFAULT_TIMEOUT_IN_MILLIS: number = 60_000; // 1 minute | ||
|
||
readonly multipartHttpClient: MultipartHttpClient<T>; | ||
|
||
readonly baseUrl: URL; | ||
|
||
readonly method: HttpMethod; | ||
|
||
readonly path: string; | ||
|
||
readonly headersValue: HeadersInit; | ||
|
||
readonly formData: FormData; | ||
|
||
readonly optionValues: MultipartHttpOptions; | ||
|
||
constructor( | ||
multipartHttpClient: MultipartHttpClient<T>, | ||
baseUrl: string, | ||
method: HttpMethod, | ||
path: string, | ||
options?: Partial<MultipartHttpOptions>, | ||
) { | ||
this.multipartHttpClient = multipartHttpClient; | ||
this.baseUrl = new URL(baseUrl); | ||
this.method = method; | ||
this.path = path; | ||
this.headersValue = {}; | ||
this.formData = new FormData(); | ||
this.optionValues = { | ||
timeoutInMillis: options?.timeoutInMillis ?? MultipartHttpRequest.DEFAULT_TIMEOUT_IN_MILLIS, | ||
onProgressCallback: options?.onProgressCallback ?? (() => {}), | ||
withCredentials: options?.withCredentials ?? false, | ||
}; | ||
} | ||
|
||
headers(headers: Record<string, string>) { | ||
Object.assign(this.headersValue, headers); | ||
return this; | ||
} | ||
|
||
data(multipartHttpData: [string, string | Blob | undefined][]) { | ||
for (const multipartHttpDataEntry of multipartHttpData) { | ||
if (multipartHttpDataEntry[1]) { | ||
this.formData.append(multipartHttpDataEntry[0], multipartHttpDataEntry[1]); | ||
} | ||
} | ||
return this; | ||
} | ||
|
||
file(file: File) { | ||
this.data([['file', file]]); | ||
return this; | ||
} | ||
|
||
files(files: File[]) { | ||
for (const file of files) { | ||
this.file(file); | ||
} | ||
return this; | ||
} | ||
|
||
buildUrl() { | ||
return encodeURI(this.baseUrl.toString() + this.path); | ||
} | ||
|
||
/** | ||
* Execute the request using the {@link multipartHttpClient}. | ||
*/ | ||
execute(): T { | ||
return this.multipartHttpClient(this); | ||
} | ||
} |