blob: 7421373ebd6cf0de6c14629067f337d2bdd4c589 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Reading a response body without letting the far end decide how much memory
* we spend. Every outbound fetch whose peer is not us belongs here.
*
* The bound is enforced twice because either check alone is a hole: the
* declared `content-length` refuses an oversized body before a byte arrives,
* and the running byte count refuses one that never declared a length or lied
* about it. Buffering first and measuring afterwards is not a bound at all,
* and `String.length` is not a byte count — a multi-byte body passes a limit
* it exceeds by up to three times.
*/
export async function readBoundedResponseText(
response: Response,
maxBytes: number,
overflow: () => Error,
): Promise<string> {
const contentLength = Number(response.headers.get('content-length'));
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
await response.body?.cancel();
throw overflow();
}
if (!response.body) return '';
const reader = response.body.getReader();
const decoder = responseTextDecoder(response);
let bytes = 0;
let text = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.byteLength;
if (bytes > maxBytes) {
await reader.cancel();
throw overflow();
}
text += decoder.decode(value, { stream: true });
}
return text + decoder.decode();
} finally {
reader.releaseLock();
}
}
function responseTextDecoder(response: Response): TextDecoder {
const contentType = response.headers.get('content-type') ?? '';
const charset = /(?:^|;)\s*charset\s*=\s*"?([^;"\s]+)/i.exec(contentType)?.[1];
if (!charset) return new TextDecoder();
try {
return new TextDecoder(charset);
} catch {
return new TextDecoder();
}
}