import Headers from './Headers.js'; import { URL, URLSearchParams } from 'url'; import Blob from '../file/Blob.js'; import FormData from '../form-data/FormData.js'; import FetchBodyUtility from './utilities/FetchBodyUtility.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import { TextDecoder } from 'util'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; /** * Fetch response. * * Based on: * https://github.com/node-fetch/node-fetch/blob/main/src/response.js (MIT) * * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response */ class Response { /** * Constructor. * * @param input Input. * @param body * @param [init] Init. */ constructor(body, init) { this._ownerDocument = null; // Public properties this.body = null; this.bodyUsed = false; this.redirected = false; this.type = 'basic'; this.url = ''; this._ownerDocument = this.constructor._ownerDocument; this.status = init?.status !== undefined ? init.status : 200; this.statusText = init?.statusText || ''; this.ok = this.status >= 200 && this.status < 300; this.headers = new Headers(init?.headers); if (body) { const { stream, contentType } = FetchBodyUtility.getBodyStream(body); this.body = stream; if (contentType && !this.headers.has('Content-Type')) { this.headers.set('Content-Type', contentType); } } } /** * Returns string tag. * * @returns String tag. */ get [Symbol.toStringTag]() { return 'Response'; } /** * Returns array buffer. * * @returns Array buffer. */ async arrayBuffer() { if (this.bodyUsed) { throw new DOMException(`Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError); } this.bodyUsed = true; const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; const taskID = taskManager.startTask(); let buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { taskManager.endTask(taskID); throw error; } taskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } /** * Returns blob. * * @returns Blob. */ async blob() { const type = this.headers.get('content-type') || ''; const buffer = await this.arrayBuffer(); return new Blob([buffer], { type }); } /** * Returns buffer. * * @returns Buffer. */ async buffer() { if (this.bodyUsed) { throw new DOMException(`Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError); } this.bodyUsed = true; const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; const taskID = taskManager.startTask(); let buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { taskManager.endTask(taskID); throw error; } taskManager.endTask(taskID); return buffer; } /** * Returns text. * * @returns Text. */ async text() { if (this.bodyUsed) { throw new DOMException(`Body has already been used for "${this.url}".`, DOMExceptionNameEnum.invalidStateError); } this.bodyUsed = true; const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; const taskID = taskManager.startTask(); let buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { taskManager.endTask(taskID); throw error; } taskManager.endTask(taskID); return new TextDecoder().decode(buffer); } /** * Returns json. * * @returns JSON. */ async json() { const text = await this.text(); return JSON.parse(text); } /** * Returns form data. * * @returns Form data. */ async formData() { const contentType = this.headers.get('content-type'); const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; const taskID = taskManager.startTask(); if (contentType.startsWith('application/x-www-form-urlencoded')) { const formData = new FormData(); let text; try { text = await this.text(); } catch (error) { taskManager.endTask(taskID); throw error; } const parameters = new URLSearchParams(text); for (const [name, value] of parameters) { formData.append(name, value); } taskManager.endTask(taskID); return formData; } let formData; try { formData = await MultipartFormDataParser.streamToFormData(this.body, contentType); } catch (error) { taskManager.endTask(taskID); throw error; } taskManager.endTask(taskID); return formData; } /** * Clones request. * * @returns Clone. */ clone() { const response = new Response(); response.status = this.status; response.statusText = this.statusText; response.ok = this.ok; response.headers = new Headers(this.headers); response.body = this.body; response.bodyUsed = this.bodyUsed; response.redirected = this.redirected; response.type = this.type; response.url = this.url; return response; } /** * Returns a redirect response. * * @param url URL. * @param status Status code. * @returns Response. */ static redirect(url, status = 302) { if (!REDIRECT_STATUS_CODES.includes(status)) { throw new DOMException('Failed to create redirect response: Invalid redirect status code.', DOMExceptionNameEnum.invalidStateError); } return new Response(null, { headers: { location: new URL(url).toString() }, status }); } /** * Returns an error response. * * @param url URL. * @param status Status code. * @returns Response. */ static error() { const response = new Response(null, { status: 0, statusText: '' }); response.type = 'error'; return response; } /** * Returns an JSON response. * * @param data Data. * @param [init] Init. * @returns Response. */ static json(data, init) { const body = JSON.stringify(data); if (body === undefined) { throw new TypeError('data is not JSON serializable'); } const headers = new Headers(init && init.headers); if (!headers.has('content-type')) { headers.set('content-type', 'application/json'); } return new Response(body, { status: 200, ...init, headers }); } } // Owner document is set by a sub-class in the Window constructor Response._ownerDocument = null; export default Response; //# sourceMappingURL=Response.js.map