mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 20:29:11 +00:00
parent
a2afae46b6
commit
c1a0d63753
2 changed files with 149 additions and 23 deletions
|
@ -779,37 +779,35 @@ class ResourceStreamResourceSink {
|
||||||
* @param {any} sink
|
* @param {any} sink
|
||||||
* @param {Uint8Array} chunk
|
* @param {Uint8Array} chunk
|
||||||
*/
|
*/
|
||||||
function readableStreamWriteChunkFn(reader, sink, chunk) {
|
async function readableStreamWriteChunkFn(reader, sink, chunk) {
|
||||||
// Empty chunk. Re-read.
|
// Empty chunk. Re-read.
|
||||||
if (chunk.length == 0) {
|
if (chunk.length == 0) {
|
||||||
readableStreamReadFn(reader, sink);
|
await readableStreamReadFn(reader, sink);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = op_readable_stream_resource_write_sync(sink.external, chunk);
|
const res = op_readable_stream_resource_write_sync(sink.external, chunk);
|
||||||
if (res == 0) {
|
if (res == 0) {
|
||||||
// Closed
|
// Closed
|
||||||
reader.cancel("resource closed");
|
await reader.cancel("resource closed");
|
||||||
sink.close();
|
sink.close();
|
||||||
} else if (res == 1) {
|
} else if (res == 1) {
|
||||||
// Successfully written (synchronous). Re-read.
|
// Successfully written (synchronous). Re-read.
|
||||||
readableStreamReadFn(reader, sink);
|
await readableStreamReadFn(reader, sink);
|
||||||
} else if (res == 2) {
|
} else if (res == 2) {
|
||||||
// Full. If the channel is full, we perform an async await until we can write, and then return
|
// Full. If the channel is full, we perform an async await until we can write, and then return
|
||||||
// to a synchronous loop.
|
// to a synchronous loop.
|
||||||
(async () => {
|
|
||||||
if (
|
if (
|
||||||
await op_readable_stream_resource_write_buf(
|
await op_readable_stream_resource_write_buf(
|
||||||
sink.external,
|
sink.external,
|
||||||
chunk,
|
chunk,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
readableStreamReadFn(reader, sink);
|
await readableStreamReadFn(reader, sink);
|
||||||
} else {
|
} else {
|
||||||
reader.cancel("resource closed");
|
await reader.cancel("resource closed");
|
||||||
sink.close();
|
sink.close();
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -822,17 +820,23 @@ function readableStreamReadFn(reader, sink) {
|
||||||
// real resource.
|
// real resource.
|
||||||
let reentrant = true;
|
let reentrant = true;
|
||||||
let gotChunk = undefined;
|
let gotChunk = undefined;
|
||||||
|
const promise = new Deferred();
|
||||||
readableStreamDefaultReaderRead(reader, {
|
readableStreamDefaultReaderRead(reader, {
|
||||||
chunkSteps(chunk) {
|
chunkSteps(chunk) {
|
||||||
// If the chunk has non-zero length, write it
|
// If the chunk has non-zero length, write it
|
||||||
if (reentrant) {
|
if (reentrant) {
|
||||||
gotChunk = chunk;
|
gotChunk = chunk;
|
||||||
} else {
|
} else {
|
||||||
readableStreamWriteChunkFn(reader, sink, chunk);
|
PromisePrototypeThen(
|
||||||
|
readableStreamWriteChunkFn(reader, sink, chunk),
|
||||||
|
() => promise.resolve(),
|
||||||
|
(e) => promise.reject(e),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeSteps() {
|
closeSteps() {
|
||||||
sink.close();
|
sink.close();
|
||||||
|
promise.resolve();
|
||||||
},
|
},
|
||||||
errorSteps(error) {
|
errorSteps(error) {
|
||||||
const success = op_readable_stream_resource_write_error(
|
const success = op_readable_stream_resource_write_error(
|
||||||
|
@ -842,15 +846,29 @@ function readableStreamReadFn(reader, sink) {
|
||||||
// We don't cancel the reader if there was an error reading. We'll let the downstream
|
// We don't cancel the reader if there was an error reading. We'll let the downstream
|
||||||
// consumer close the resource after it receives the error.
|
// consumer close the resource after it receives the error.
|
||||||
if (!success) {
|
if (!success) {
|
||||||
reader.cancel("resource closed");
|
PromisePrototypeThen(
|
||||||
}
|
reader.cancel("resource closed"),
|
||||||
|
() => {
|
||||||
sink.close();
|
sink.close();
|
||||||
|
promise.resolve();
|
||||||
|
},
|
||||||
|
(e) => promise.reject(e),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sink.close();
|
||||||
|
promise.resolve();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
reentrant = false;
|
reentrant = false;
|
||||||
if (gotChunk) {
|
if (gotChunk) {
|
||||||
readableStreamWriteChunkFn(reader, sink, gotChunk);
|
PromisePrototypeThen(
|
||||||
|
readableStreamWriteChunkFn(reader, sink, gotChunk),
|
||||||
|
() => promise.resolve(),
|
||||||
|
(e) => promise.reject(e),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return promise.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -873,7 +891,9 @@ function resourceForReadableStream(stream, length) {
|
||||||
PromisePrototypeCatch(
|
PromisePrototypeCatch(
|
||||||
PromisePrototypeThen(
|
PromisePrototypeThen(
|
||||||
op_readable_stream_resource_await_close(rid),
|
op_readable_stream_resource_await_close(rid),
|
||||||
() => reader.cancel("resource closed"),
|
() => {
|
||||||
|
PromisePrototypeCatch(reader.cancel("resource closed"), () => {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
@ -884,7 +904,9 @@ function resourceForReadableStream(stream, length) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger the first read
|
// Trigger the first read
|
||||||
readableStreamReadFn(reader, sink);
|
PromisePrototypeCatch(readableStreamReadFn(reader, sink), (err) => {
|
||||||
|
PromisePrototypeCatch(reader.cancel(err), () => {});
|
||||||
|
});
|
||||||
|
|
||||||
return rid;
|
return rid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -547,3 +547,107 @@ Deno.test(function readableStreamFromWithStringThrows() {
|
||||||
"Failed to execute 'ReadableStream.from': Argument 1 can not be converted to async iterable.",
|
"Failed to execute 'ReadableStream.from': Argument 1 can not be converted to async iterable.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test(async function readableStreamFromWithStringThrows() {
|
||||||
|
const serverPort = 4592;
|
||||||
|
const upstreamServerPort = 4593;
|
||||||
|
|
||||||
|
const stopSignal = new AbortController();
|
||||||
|
const promise = Promise.withResolvers();
|
||||||
|
// Response transforming server that crashes with an uncaught AbortError.
|
||||||
|
function startServer() {
|
||||||
|
Deno.serve({ port: serverPort, signal: stopSignal.signal }, async (req) => {
|
||||||
|
const upstreamResponse = await fetch(
|
||||||
|
`http://localhost:${upstreamServerPort}`,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use a TransformStream to convert the response body to uppercase.
|
||||||
|
const transformStream = new TransformStream({
|
||||||
|
transform(chunk, controller) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const chunk2 = encoder.encode(decoder.decode(chunk).toUpperCase());
|
||||||
|
controller.enqueue(chunk2);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
upstreamResponse.body?.pipeTo(transformStream.writable).catch(() => {});
|
||||||
|
|
||||||
|
return new Response(transformStream.readable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== THE ISSUE IS NOT IN THE CODE BELOW ====
|
||||||
|
|
||||||
|
// Upstream server that sends a response with a body that never ends.
|
||||||
|
// This is not where the error happens (it handlers the cancellation correctly).
|
||||||
|
function startUpstreamServer() {
|
||||||
|
Deno.serve({ port: upstreamServerPort, signal: stopSignal.signal }, (_) => {
|
||||||
|
// Create an infinite readable stream that emits 'a'
|
||||||
|
let pushTimeout: number | null = null;
|
||||||
|
const readableStream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const chunk = encoder.encode("a");
|
||||||
|
|
||||||
|
function push() {
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
pushTimeout = setTimeout(push, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
push();
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel(reason) {
|
||||||
|
assertEquals(reason, "resource closed");
|
||||||
|
promise.resolve(undefined);
|
||||||
|
clearTimeout(pushTimeout!);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(readableStream, {
|
||||||
|
headers: { "Content-Type": "text/plain" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client is just there to simulate a client that cancels a request.
|
||||||
|
async function startClient() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:${serverPort}`, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("client: failed to get reader from response");
|
||||||
|
}
|
||||||
|
|
||||||
|
let received = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
received += value.length;
|
||||||
|
|
||||||
|
if (received >= 5) {
|
||||||
|
controller.abort();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startUpstreamServer();
|
||||||
|
startServer();
|
||||||
|
const p = startClient();
|
||||||
|
|
||||||
|
await promise.promise;
|
||||||
|
stopSignal.abort();
|
||||||
|
await p;
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue