Skip to content

Commit

Permalink
Response.clone, test, http extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
kflansburg committed May 15, 2024
1 parent fbeed9d commit d24cc39
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 28 deletions.
36 changes: 34 additions & 2 deletions worker-sandbox/src/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use super::{ApiData, SomeSharedData};
use futures_util::future::Either;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use worker::{
wasm_bindgen_futures, AbortController, Delay, Env, Fetch, Method, Request, RequestInit,
Response, Result,
wasm_bindgen_futures, AbortController, Delay, EncodeBody, Env, Fetch, Method, Request,
RequestInit, Response, Result,
};

#[worker::send]
Expand Down Expand Up @@ -177,3 +178,34 @@ pub async fn handle_cloned_fetch(

Response::ok((left == right).to_string())
}

#[worker::send]
pub async fn handle_cloned_response_attributes(
_req: Request,
_env: Env,
_data: SomeSharedData,
) -> Result<Response> {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct TestCf {
foo: String,
}
let mut resp = Response::builder()
.with_status(200)
.with_encode_body(EncodeBody::Manual)
.with_cf(TestCf {
foo: "bar".to_owned(),
})?
.empty();

let resp1 = resp.cloned()?;

assert!(matches!(resp.encode_body(), EncodeBody::Manual));
assert!(matches!(resp1.encode_body(), EncodeBody::Manual));

let cf: TestCf = resp.cf()?.unwrap();
let cf1: TestCf = resp1.cf()?.unwrap();

assert_eq!(cf, cf1);

Response::ok("true")
}
8 changes: 8 additions & 0 deletions worker-sandbox/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ pub fn make_router(data: SomeSharedData, env: Env) -> axum::Router {
get(handler!(request::handle_cloned_stream)),
)
.route("/cloned-fetch", get(handler!(fetch::handle_cloned_fetch)))
.route(
"/cloned-response",
get(handler!(fetch::handle_cloned_response_attributes)),
)
.route("/wait/:delay", get(handler!(request::handle_wait_delay)))
.route(
"/custom-response-body",
Expand Down Expand Up @@ -300,6 +304,10 @@ pub fn make_router<'a>(data: SomeSharedData) -> Router<'a, SomeSharedData> {
.get_async("/cloned", handler!(request::handle_cloned))
.get_async("/cloned-stream", handler!(request::handle_cloned_stream))
.get_async("/cloned-fetch", handler!(fetch::handle_cloned_fetch))
.get_async(
"/cloned-response",
handler!(fetch::handle_cloned_response_attributes),
)
.get_async("/wait/:delay", handler!(request::handle_wait_delay))
.get_async(
"/custom-response-body",
Expand Down
5 changes: 5 additions & 0 deletions worker-sandbox/tests/clone.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ describe("cache", () => {
const resp = await mf.dispatchFetch("https://fake.host/cloned-fetch");
expect(await resp.text()).toBe("true");
});

test("cloned response", async () => {
const resp = await mf.dispatchFetch("https://fake.host/cloned-response");
expect(await resp.text()).toBe("true");
});
});
17 changes: 16 additions & 1 deletion worker-sys/src/ext/response.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use wasm_bindgen::prelude::*;

mod glue {

use super::*;

#[wasm_bindgen]
Expand All @@ -10,16 +11,30 @@ mod glue {

#[wasm_bindgen(method, catch, getter)]
pub fn webSocket(this: &Response) -> Result<Option<web_sys::WebSocket>, JsValue>;

#[wasm_bindgen(method, catch, getter)]
pub fn cf(this: &Response) -> Result<Option<js_sys::Object>, JsValue>;
}
}

pub trait ResponseExt {
/// Getter for the `webSocket` field of this object.
fn websocket(&self) -> Option<web_sys::WebSocket>;

/// Getter for the `cf` field of this object.
fn cf(&self) -> Option<js_sys::Object>;
}

impl ResponseExt for web_sys::Response {
fn websocket(&self) -> Option<web_sys::WebSocket> {
self.unchecked_ref::<glue::Response>().webSocket().unwrap()
self.unchecked_ref::<glue::Response>()
.webSocket()
.expect("read response.webSocket")
}

fn cf(&self) -> Option<js_sys::Object> {
self.unchecked_ref::<glue::Response>()
.cf()
.expect("read response.cf")
}
}
19 changes: 19 additions & 0 deletions worker/src/cf.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#[cfg(feature = "timezone")]
use crate::Result;

use serde::de::DeserializeOwned;
use wasm_bindgen::JsCast;

/// In addition to the methods on the `Request` struct, the `Cf` struct on an inbound Request contains information about the request provided by Cloudflare’s edge.
///
/// [Details](https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties)
Expand Down Expand Up @@ -254,3 +257,19 @@ impl From<worker_sys::TlsClientAuth> for TlsClientAuth {
Self { inner }
}
}

#[derive(Clone)]
pub struct CfResponseProperties(pub(crate) js_sys::Object);

impl CfResponseProperties {
pub fn into_raw(self) -> js_sys::Object {
self.0
}

pub fn try_into<T: DeserializeOwned>(self) -> crate::Result<T> {
Ok(serde_wasm_bindgen::from_value(self.0.unchecked_into())?)
}
}

unsafe impl Send for CfResponseProperties {}
unsafe impl Sync for CfResponseProperties {}
28 changes: 22 additions & 6 deletions worker/src/http/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ use crate::WebSocket;
use bytes::Bytes;

use crate::http::body::BodyStream;
use crate::response::EncodeBody;
use crate::CfResponseProperties;
use crate::Headers;
use crate::ResponseBuilder;
use worker_sys::ext::ResponseExt;
use worker_sys::ext::ResponseInitExt;

/// **Requires** `http` feature. Convert generic [`http::Response<B>`](crate::HttpResponse)
/// to [`web_sys::Resopnse`](web_sys::Response) where `B` can be any [`http_body::Body`](http_body::Body)
pub fn to_wasm<B>(mut res: http::Response<B>) -> Result<web_sys::Response>
where
B: http_body::Body<Data = Bytes> + 'static,
{
let mut init = web_sys::ResponseInit::new();
init.status(res.status().as_u16());
let headers = web_sys_headers_from_header_map(res.headers())?;
init.headers(headers.as_ref());
let mut init = ResponseBuilder::new()
.with_status(res.status().as_u16())
.with_headers(Headers(headers));

if let Some(ws) = res.extensions_mut().remove::<WebSocket>() {
init.websocket(ws.as_ref())?;
init = init.with_websocket(ws);
}
if let Some(encode_body) = res.extensions_mut().remove::<EncodeBody>() {
init = init.with_encode_body(encode_body);
}
if let Some(CfResponseProperties(obj)) = res.extensions_mut().remove::<CfResponseProperties>() {
init = init.with_cf_raw(obj);
}

let body = res.into_body();
Expand All @@ -37,7 +47,7 @@ where

Ok(web_sys::Response::new_with_opt_readable_stream_and_init(
readable_stream.as_ref(),
&init,
&init.into(),
)?)
}

Expand All @@ -52,6 +62,9 @@ pub fn from_wasm(res: web_sys::Response) -> Result<HttpResponse> {
if let Some(ws) = res.websocket() {
builder = builder.extension(WebSocket::from(ws));
}
if let Some(cf) = res.cf() {
builder = builder.extension(CfResponseProperties(cf));
}
Ok(if let Some(body) = res.body() {
builder.body(Body::new(body))?
} else {
Expand All @@ -71,6 +84,9 @@ impl From<crate::Response> for http::Response<axum::body::Body> {
if let Some(ws) = res.websocket() {
builder = builder.extension(WebSocket::from(ws));
}
if let Some(cf) = res.cf() {
builder = builder.extension(CfResponseProperties(cf));
}
if let Some(body) = res.body() {
builder
.body(axum::body::Body::new(crate::Body::new(body)))
Expand Down
4 changes: 2 additions & 2 deletions worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub use wasm_bindgen;
pub use wasm_bindgen_futures;
pub use worker_kv as kv;

pub use cf::{Cf, TlsClientAuth};
pub use cf::{Cf, CfResponseProperties, TlsClientAuth};
pub use worker_macros::{durable_object, event, send};
#[doc(hidden)]
pub use worker_sys;
Expand Down Expand Up @@ -184,7 +184,7 @@ pub use crate::queue::*;
pub use crate::r2::*;
pub use crate::request::{FromRequest, Request};
pub use crate::request_init::*;
pub use crate::response::{IntoResponse, Response, ResponseBody, ResponseBuilder};
pub use crate::response::{EncodeBody, IntoResponse, Response, ResponseBody, ResponseBuilder};
pub use crate::router::{RouteContext, RouteParams, Router};
pub use crate::schedule::*;
pub use crate::socket::*;
Expand Down
48 changes: 31 additions & 17 deletions worker/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ impl Response {
self
}

/// Read the `encode_body` configuration for this `Response`.
pub fn encode_body(&self) -> &EncodeBody {
&self.init.encode_body
}

/// Set this response's `encodeBody` option.
/// In most cases this is not needed, but it can be set to "manual" to
/// return already compressed data to the user without re-compression.
Expand All @@ -261,7 +266,18 @@ impl Response {
self
}

/// Set this response's `cf` options.
/// Read the `cf` information for this `Response`.
pub fn cf<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>> {
self.init
.cf
.clone()
.map(|cf| serde_wasm_bindgen::from_value(cf.unchecked_into()))
.transpose()
.map_err(Error::SerdeWasmBindgenError)
}

/// Set this response's `cf` options. This is used by consumers of the `Response` for
/// informational purposes and has no impact on Workers behavior.
pub fn with_cf<T: serde::Serialize>(mut self, cf: Option<T>) -> Result<Self> {
match cf {
Some(cf) => self.init = self.init.with_cf(cf)?,
Expand Down Expand Up @@ -291,15 +307,6 @@ impl Response {
if self.init.websocket.is_some() {
return Err(Error::RustError("WebSockets cannot be cloned".into()));
}
// This information is lost when we make the roundtrip though web_sys::Response
if matches!(self.init.encode_body, EncodeBody::Manual) {
return Err(Error::RustError(
"manual encode_body cannot be cloned".into(),
));
}
if self.init.cf.is_some() {
return Err(Error::RustError("cf cannot be cloned".into()));
}

let edge = web_sys::Response::from(&*self);
let cloned = edge.clone()?;
Expand All @@ -311,7 +318,9 @@ impl Response {
None => ResponseBody::Empty,
};

Ok(cloned.into())
let clone: Response = cloned.into();

Ok(clone.with_encode_body(*self.encode_body()))
}
}

Expand All @@ -323,7 +332,7 @@ fn no_using_invalid_error_status_code() {
}

#[non_exhaustive]
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug, Clone, Copy)]
/// Control how the body of the response will be encoded by the runtime before
/// it is returned to the user.
pub enum EncodeBody {
Expand Down Expand Up @@ -405,18 +414,23 @@ impl ResponseBuilder {
self
}

/// Set this response's `cf` options.
pub fn with_cf<T: serde::Serialize>(mut self, cf: T) -> Result<Self> {
/// Set this response's `cf` options. This is used by consumers of the `Response` for
/// informational purposes and has no impact on Workers behavior.
pub fn with_cf<T: serde::Serialize>(self, cf: T) -> Result<Self> {
let value = serde_wasm_bindgen::to_value(&cf)?;
if value.is_object() {
let obj = value.unchecked_into::<js_sys::Object>();
self.cf = Some(obj);
Ok(self)
Ok(self.with_cf_raw(obj))
} else {
Err(Error::from("cf must be an object"))
}
}

pub(crate) fn with_cf_raw(mut self, obj: js_sys::Object) -> Self {
self.cf = Some(obj);
self
}

/// Build a response with a fixed-length body.
pub fn fixed(self, body: Vec<u8>) -> Response {
Response {
Expand Down Expand Up @@ -596,7 +610,7 @@ impl From<web_sys::Response> for Response {
status_code: res.status(),
websocket: res.websocket().map(|ws| ws.into()),
encode_body: EncodeBody::Automatic,
cf: None,
cf: res.cf(),
};
match res.body() {
Some(stream) => builder.stream(stream),
Expand Down

0 comments on commit d24cc39

Please sign in to comment.