cloudflare / pingora

A library for building fast, reliable and evolvable network services.
Apache License 2.0
22.13k stars 1.23k forks source link

Send error context to downstream client #457

Open cetra3 opened 1 week ago

cetra3 commented 1 week ago

What is the problem your feature solves, or the need it fulfills?

At the moment the context of the error is logged, but that information isn't sent to the downstream client. We're using pingora for some internal proxying, and while we can get an error status back when things go wrong, the context is lost. It'd be great if there was a way to send an error back to the client.

Describe the solution you'd like

A way to specify that the error context is returned in the response body to clients.

Describe alternatives you've considered

As the error is happening within the upstream_peer of the ProxyHttp trait, I tried setting the session's downstream response body directly, but this doesn't appear to work. Instead no body is sent back to the client downstream.

I.e, doing something like this:

               // Send error to downstream client
                let error_msg = pingora_error
                    .context
                    .as_ref()
                    .map(|val| Bytes::from(val.to_string().into_bytes()));

                session.as_downstream_mut().write_response_body(error_msg.unwrap_or_default(), true).await?;

And also tried write_response_body directly:

                session.write_response_body(error_msg, true).await?;
andrewhavck commented 1 week ago

Where are you trying to write the error? Ideally this is would happen in fail_to_proxy.

/// Users may write an error response to the downstream if the downstream is still writable.

cetra3 commented 1 week ago

Implementing the fail_to_proxy method doesn't work either:


    async fn fail_to_proxy(&self, session: &mut Session, e: &Error, _ctx: &mut Self::CTX) -> u16
    where
        Self::CTX: Send + Sync,
    {
        let server_session = session.as_mut();
        let code = match e.etype() {
            HTTPStatus(code) => *code,
            _ => {
                match e.esource() {
                    ErrorSource::Upstream => 502,
                    ErrorSource::Downstream => {
                        match e.etype() {
                            WriteError | ReadError | ConnectionClosed => {
                                /* conn already dead */
                                0
                            }
                            _ => 400,
                        }
                    }
                    ErrorSource::Internal | ErrorSource::Unset => 500,
                }
            }
        };
        if code > 0 {
            server_session.respond_error(code).await;
            if let Some(ref ctx) = e.context {
                // Nothing is written to the downstream client
                server_session.write_response_body(ctx.to_string().into(), true).await.ok();
            }
        }
        code
    }
andrewhavck commented 6 days ago

The respond_error() function uses gen_error_response() which has a default Content-Length: 0. so the body is being ignored when you try to write it.

You can either calculate the length of the error message and set the correct content length or use chunked encoding on the error response. Either way you'll need to construct your own error response headers.

IE:

fn gen_chunked_error_response(code: u16) -> ResponseHeader {
    let mut resp = ResponseHeader::build(code, Some(4)).unwrap();
    resp.insert_header(header::SERVER, &SERVER_NAME[..])
        .unwrap();
    resp.insert_header(header::DATE, "Sun, 06 Nov 1994 08:49:37 GMT")
        .unwrap(); // placeholder
    resp.insert_header(header::TRANSFER_ENCODING, "chunked")
        .unwrap();
    resp.insert_header(header::CACHE_CONTROL, "private, no-store")
        .unwrap();
    resp
}

Then use write_response_header() and write_response_body().

We actually have some code that handles error responses with bodies internally, will move this out so this is easier for others.