/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/ssr/node/src/response.ts
161 строка
5 KB
Alan Agius
refactor(@angular/ssr): simplify response destroyed/closed check
10 авг 2026, 16:21
10 авг 2026, 16:21
36dff80
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import type { ServerResponse } from 'node:http'; import type { Http2ServerResponse } from 'node:http2'; /** * Checks whether a Node.js `ServerResponse` or `Http2ServerResponse` is destroyed, closed, or ended. * * @param destination - The HTTP/1.1 or HTTP/2 server response to check. * @returns `true` if the response or its underlying stream is destroyed, closed, or ended; otherwise `false`. */ function isResponseDestroyedOrClosed(destination: ServerResponse | Http2ServerResponse): boolean { return ( destination.destroyed || destination.closed || destination.writableEnded || ('stream' in destination && (!destination.stream || destination.stream.destroyed || destination.stream.closed)) ); } /** * Streams a web-standard `Response` into a Node.js `ServerResponse` * or `Http2ServerResponse`. * * This function adapts the web `Response` object to write its content * to a Node.js response object, handling both HTTP/1.1 and HTTP/2. * * @param source - The web-standard `Response` object to stream from. * @param destination - The Node.js response object (`ServerResponse` or `Http2ServerResponse`) to stream into. * @returns A promise that resolves once the streaming operation is complete. */ export async function writeResponseToNodeResponse( source: Response, destination: ServerResponse | Http2ServerResponse, ): Promise<void> { if (isResponseDestroyedOrClosed(destination)) { return; } const { status, headers, body } = source; destination.statusCode = status; let cookieHeaderSet = false; for (const [name, value] of headers.entries()) { if (name === 'set-cookie') { if (cookieHeaderSet) { continue; } // Sets the 'set-cookie' header only once to ensure it is correctly applied. // Concatenating 'set-cookie' values can lead to incorrect behavior, so we use a single value from `headers.getSetCookie()`. destination.setHeader(name, headers.getSetCookie()); cookieHeaderSet = true; } else { destination.setHeader(name, value); } } if ('flushHeaders' in destination) { destination.flushHeaders(); } if (!body) { if (!isResponseDestroyedOrClosed(destination)) { destination.end(); } return; } let isClosed = isResponseDestroyedOrClosed(destination); const isDestroyedOrClosed = () => isClosed || isResponseDestroyedOrClosed(destination); let readerCancelled = false; const reader = body.getReader(); const cancelReader = (error?: unknown) => { if (readerCancelled) { return; } readerCancelled = true; isClosed = true; destination.off('close', cancelReader); destination.off('error', cancelReader); reader.cancel(error).catch((err) => { // eslint-disable-next-line no-console console.error( `An error occurred while writing the response body for: ${destination.req.url}.`, err, ); }); }; destination.once('close', cancelReader); destination.once('error', cancelReader); try { // eslint-disable-next-line no-constant-condition while (true) { if (isDestroyedOrClosed()) { cancelReader(); break; } const { done, value } = await reader.read(); if (isDestroyedOrClosed()) { cancelReader(); break; } if (done) { destination.end(); break; } const canContinue = (destination as ServerResponse).write(value); if (canContinue === false) { // Explicitly check for `false`, as AWS may return `undefined` even though this is not valid. // See: https://github.com/CodeGenieApp/serverless-express/issues/683 await new Promise<void>((resolve) => { if (isDestroyedOrClosed()) { resolve(); return; } const onDrain = () => { destination.off('close', onClose); destination.off('error', onClose); resolve(); }; const onClose = () => { destination.off('drain', onDrain); destination.off('close', onClose); destination.off('error', onClose); cancelReader(); resolve(); }; destination.once('drain', onDrain); destination.once('close', onClose); destination.once('error', onClose); }); } } } catch { if (!isDestroyedOrClosed()) { destination.end('Internal server error.'); } } finally { destination.off('close', cancelReader); destination.off('error', cancelReader); } }