mirror of
https://github.com/denoland/deno.git
synced 2025-01-12 00:54:02 -05:00
feat(ext/http): Add support for trailers w/internal API (HTTP/2 only) (#19182)
Necessary for #3326. Requested in #10214 as well.
This commit is contained in:
parent
5b07522349
commit
2b92efa645
6 changed files with 103 additions and 2 deletions
|
@ -15,6 +15,7 @@ import {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
upgradeHttpRaw,
|
upgradeHttpRaw,
|
||||||
|
addTrailers,
|
||||||
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
|
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
|
||||||
} = Deno[Deno.internal];
|
} = Deno[Deno.internal];
|
||||||
|
|
||||||
|
@ -2903,6 +2904,45 @@ Deno.test(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO(mmastrac): This test should eventually use fetch, when we support trailers there.
|
||||||
|
// This test is ignored because it's flaky and relies on cURL's verbose output.
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { net: true, run: true, read: true }, ignore: true },
|
||||||
|
async function httpServerTrailers() {
|
||||||
|
const ac = new AbortController();
|
||||||
|
const listeningPromise = deferred();
|
||||||
|
|
||||||
|
const server = Deno.serve({
|
||||||
|
handler: () => {
|
||||||
|
const response = new Response("Hello World", {
|
||||||
|
headers: {
|
||||||
|
"trailer": "baz",
|
||||||
|
"transfer-encoding": "chunked",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
addTrailers(response, [["baz", "why"]]);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
port: 4501,
|
||||||
|
signal: ac.signal,
|
||||||
|
onListen: onListen(listeningPromise),
|
||||||
|
onError: createOnErrorCb(ac),
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't have a great way to access this right now, so just fetch the trailers with cURL
|
||||||
|
const [_, stderr] = await curlRequestWithStdErr([
|
||||||
|
"http://localhost:4501/path",
|
||||||
|
"-v",
|
||||||
|
"--http2",
|
||||||
|
"--http2-prior-knowledge",
|
||||||
|
]);
|
||||||
|
assertMatch(stderr, /baz: why/);
|
||||||
|
ac.abort();
|
||||||
|
await server;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Deno.test(
|
Deno.test(
|
||||||
{ permissions: { net: true, run: true, read: true } },
|
{ permissions: { net: true, run: true, read: true } },
|
||||||
async function httpsServeCurlH2C() {
|
async function httpsServeCurlH2C() {
|
||||||
|
@ -2948,3 +2988,13 @@ async function curlRequest(args: string[]) {
|
||||||
assert(success);
|
assert(success);
|
||||||
return new TextDecoder().decode(stdout);
|
return new TextDecoder().decode(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function curlRequestWithStdErr(args: string[]) {
|
||||||
|
const { success, stdout, stderr } = await new Deno.Command("curl", {
|
||||||
|
args,
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "piped",
|
||||||
|
}).output();
|
||||||
|
assert(success);
|
||||||
|
return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)];
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ const {
|
||||||
op_http_set_response_body_text,
|
op_http_set_response_body_text,
|
||||||
op_http_set_response_header,
|
op_http_set_response_header,
|
||||||
op_http_set_response_headers,
|
op_http_set_response_headers,
|
||||||
|
op_http_set_response_trailers,
|
||||||
op_http_upgrade_raw,
|
op_http_upgrade_raw,
|
||||||
op_http_upgrade_websocket_next,
|
op_http_upgrade_websocket_next,
|
||||||
op_http_wait,
|
op_http_wait,
|
||||||
|
@ -75,6 +76,7 @@ const {
|
||||||
"op_http_set_response_body_text",
|
"op_http_set_response_body_text",
|
||||||
"op_http_set_response_header",
|
"op_http_set_response_header",
|
||||||
"op_http_set_response_headers",
|
"op_http_set_response_headers",
|
||||||
|
"op_http_set_response_trailers",
|
||||||
"op_http_upgrade_raw",
|
"op_http_upgrade_raw",
|
||||||
"op_http_upgrade_websocket_next",
|
"op_http_upgrade_websocket_next",
|
||||||
"op_http_wait",
|
"op_http_wait",
|
||||||
|
@ -125,6 +127,11 @@ function upgradeHttpRaw(req, conn) {
|
||||||
throw new TypeError("upgradeHttpRaw may only be used with Deno.serve");
|
throw new TypeError("upgradeHttpRaw may only be used with Deno.serve");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addTrailers(resp, headerList) {
|
||||||
|
const inner = toInnerResponse(resp);
|
||||||
|
op_http_set_response_trailers(inner.slabId, headerList);
|
||||||
|
}
|
||||||
|
|
||||||
class InnerRequest {
|
class InnerRequest {
|
||||||
#slabId;
|
#slabId;
|
||||||
#context;
|
#context;
|
||||||
|
@ -687,6 +694,7 @@ function serve(arg1, arg2) {
|
||||||
return { finished };
|
return { finished };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internals.addTrailers = addTrailers;
|
||||||
internals.upgradeHttpRaw = upgradeHttpRaw;
|
internals.upgradeHttpRaw = upgradeHttpRaw;
|
||||||
|
|
||||||
export { serve, upgradeHttpRaw };
|
export { serve, upgradeHttpRaw };
|
||||||
|
|
|
@ -318,6 +318,22 @@ pub fn op_http_set_response_headers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[op]
|
||||||
|
pub fn op_http_set_response_trailers(
|
||||||
|
slab_id: SlabId,
|
||||||
|
trailers: Vec<(ByteString, ByteString)>,
|
||||||
|
) {
|
||||||
|
let mut http = slab_get(slab_id);
|
||||||
|
let mut trailer_map: HeaderMap = HeaderMap::with_capacity(trailers.len());
|
||||||
|
for (name, value) in trailers {
|
||||||
|
// These are valid latin-1 strings
|
||||||
|
let name = HeaderName::from_bytes(&name).unwrap();
|
||||||
|
let value = HeaderValue::from_bytes(&value).unwrap();
|
||||||
|
trailer_map.append(name, value);
|
||||||
|
}
|
||||||
|
*http.trailers().borrow_mut() = Some(trailer_map);
|
||||||
|
}
|
||||||
|
|
||||||
fn is_request_compressible(headers: &HeaderMap) -> Compression {
|
fn is_request_compressible(headers: &HeaderMap) -> Compression {
|
||||||
let Some(accept_encoding) = headers.get(ACCEPT_ENCODING) else {
|
let Some(accept_encoding) = headers.get(ACCEPT_ENCODING) else {
|
||||||
return Compression::None;
|
return Compression::None;
|
||||||
|
|
|
@ -116,6 +116,7 @@ deno_core::extension!(
|
||||||
http_next::op_http_set_response_body_text,
|
http_next::op_http_set_response_body_text,
|
||||||
http_next::op_http_set_response_header,
|
http_next::op_http_set_response_header,
|
||||||
http_next::op_http_set_response_headers,
|
http_next::op_http_set_response_headers,
|
||||||
|
http_next::op_http_set_response_trailers,
|
||||||
http_next::op_http_track,
|
http_next::op_http_track,
|
||||||
http_next::op_http_upgrade_websocket_next,
|
http_next::op_http_upgrade_websocket_next,
|
||||||
http_next::op_http_upgrade_raw,
|
http_next::op_http_upgrade_raw,
|
||||||
|
|
|
@ -158,7 +158,11 @@ impl std::fmt::Debug for ResponseBytesInner {
|
||||||
/// required by hyper. As the API requires information about request completion (including a success/fail
|
/// required by hyper. As the API requires information about request completion (including a success/fail
|
||||||
/// flag), we include a very lightweight [`CompletionHandle`] for interested parties to listen on.
|
/// flag), we include a very lightweight [`CompletionHandle`] for interested parties to listen on.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ResponseBytes(ResponseBytesInner, CompletionHandle);
|
pub struct ResponseBytes(
|
||||||
|
ResponseBytesInner,
|
||||||
|
CompletionHandle,
|
||||||
|
Rc<RefCell<Option<HeaderMap>>>,
|
||||||
|
);
|
||||||
|
|
||||||
impl ResponseBytes {
|
impl ResponseBytes {
|
||||||
pub fn initialize(&mut self, inner: ResponseBytesInner) {
|
pub fn initialize(&mut self, inner: ResponseBytesInner) {
|
||||||
|
@ -170,6 +174,10 @@ impl ResponseBytes {
|
||||||
self.1.clone()
|
self.1.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trailers(&self) -> Rc<RefCell<Option<HeaderMap>>> {
|
||||||
|
self.2.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn complete(&mut self, success: bool) -> ResponseBytesInner {
|
fn complete(&mut self, success: bool) -> ResponseBytesInner {
|
||||||
if matches!(self.0, ResponseBytesInner::Done) {
|
if matches!(self.0, ResponseBytesInner::Done) {
|
||||||
return ResponseBytesInner::Done;
|
return ResponseBytesInner::Done;
|
||||||
|
@ -250,6 +258,9 @@ impl Body for ResponseBytes {
|
||||||
let res = loop {
|
let res = loop {
|
||||||
let res = match &mut self.0 {
|
let res = match &mut self.0 {
|
||||||
ResponseBytesInner::Done | ResponseBytesInner::Empty => {
|
ResponseBytesInner::Done | ResponseBytesInner::Empty => {
|
||||||
|
if let Some(trailers) = self.2.borrow_mut().take() {
|
||||||
|
return std::task::Poll::Ready(Some(Ok(Frame::trailers(trailers))));
|
||||||
|
}
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
ResponseBytesInner::Bytes(..) => {
|
ResponseBytesInner::Bytes(..) => {
|
||||||
|
@ -271,6 +282,9 @@ impl Body for ResponseBytes {
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches!(res, ResponseStreamResult::EndOfStream) {
|
if matches!(res, ResponseStreamResult::EndOfStream) {
|
||||||
|
if let Some(trailers) = self.2.borrow_mut().take() {
|
||||||
|
return std::task::Poll::Ready(Some(Ok(Frame::trailers(trailers))));
|
||||||
|
}
|
||||||
self.complete(true);
|
self.complete(true);
|
||||||
}
|
}
|
||||||
std::task::Poll::Ready(res.into())
|
std::task::Poll::Ready(res.into())
|
||||||
|
@ -278,6 +292,7 @@ impl Body for ResponseBytes {
|
||||||
|
|
||||||
fn is_end_stream(&self) -> bool {
|
fn is_end_stream(&self) -> bool {
|
||||||
matches!(self.0, ResponseBytesInner::Done | ResponseBytesInner::Empty)
|
matches!(self.0, ResponseBytesInner::Done | ResponseBytesInner::Empty)
|
||||||
|
&& self.2.borrow_mut().is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self) -> SizeHint {
|
fn size_hint(&self) -> SizeHint {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::response_body::CompletionHandle;
|
||||||
use crate::response_body::ResponseBytes;
|
use crate::response_body::ResponseBytes;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use http::request::Parts;
|
use http::request::Parts;
|
||||||
|
use http::HeaderMap;
|
||||||
use hyper1::body::Incoming;
|
use hyper1::body::Incoming;
|
||||||
use hyper1::upgrade::OnUpgrade;
|
use hyper1::upgrade::OnUpgrade;
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ use slab::Slab;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::cell::RefMut;
|
use std::cell::RefMut;
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
pub type Request = hyper1::Request<Incoming>;
|
pub type Request = hyper1::Request<Incoming>;
|
||||||
pub type Response = hyper1::Response<ResponseBytes>;
|
pub type Response = hyper1::Response<ResponseBytes>;
|
||||||
|
@ -23,6 +25,7 @@ pub struct HttpSlabRecord {
|
||||||
// The response may get taken before we tear this down
|
// The response may get taken before we tear this down
|
||||||
response: Option<Response>,
|
response: Option<Response>,
|
||||||
promise: CompletionHandle,
|
promise: CompletionHandle,
|
||||||
|
trailers: Rc<RefCell<Option<HeaderMap>>>,
|
||||||
been_dropped: bool,
|
been_dropped: bool,
|
||||||
#[cfg(feature = "__zombie_http_tracking")]
|
#[cfg(feature = "__zombie_http_tracking")]
|
||||||
alive: bool,
|
alive: bool,
|
||||||
|
@ -81,11 +84,14 @@ fn slab_insert_raw(
|
||||||
) -> SlabId {
|
) -> SlabId {
|
||||||
let index = SLAB.with(|slab| {
|
let index = SLAB.with(|slab| {
|
||||||
let mut slab = slab.borrow_mut();
|
let mut slab = slab.borrow_mut();
|
||||||
|
let body = ResponseBytes::default();
|
||||||
|
let trailers = body.trailers();
|
||||||
slab.insert(HttpSlabRecord {
|
slab.insert(HttpSlabRecord {
|
||||||
request_info,
|
request_info,
|
||||||
request_parts,
|
request_parts,
|
||||||
request_body,
|
request_body,
|
||||||
response: Some(Response::new(ResponseBytes::default())),
|
response: Some(Response::new(body)),
|
||||||
|
trailers,
|
||||||
been_dropped: false,
|
been_dropped: false,
|
||||||
promise: CompletionHandle::default(),
|
promise: CompletionHandle::default(),
|
||||||
#[cfg(feature = "__zombie_http_tracking")]
|
#[cfg(feature = "__zombie_http_tracking")]
|
||||||
|
@ -182,6 +188,11 @@ impl SlabEntry {
|
||||||
self.self_mut().response.as_mut().unwrap()
|
self.self_mut().response.as_mut().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the trailers.
|
||||||
|
pub fn trailers(&mut self) -> &RefCell<Option<HeaderMap>> {
|
||||||
|
&self.self_mut().trailers
|
||||||
|
}
|
||||||
|
|
||||||
/// Take the response.
|
/// Take the response.
|
||||||
pub fn take_response(&mut self) -> Response {
|
pub fn take_response(&mut self) -> Response {
|
||||||
self.self_mut().response.take().unwrap()
|
self.self_mut().response.take().unwrap()
|
||||||
|
|
Loading…
Reference in a new issue