Skip to content

Commit a8a6e77

Browse files
andrewhavckeaufavor
authored andcommitted
Improve support for sending custom response headers and bodies for error messages
1 parent e309436 commit a8a6e77

File tree

8 files changed

+74
-52
lines changed

8 files changed

+74
-52
lines changed

.bleep

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
89319e9383d6f99066dfeace750a553d45e1c167
1+
7dbf3a97f9e59d8ad1d8e5d199cae6ee49869b9c

pingora-core/src/protocols/http/server.rs

+22-9
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use crate::protocols::{Digest, SocketAddr, Stream};
2222
use bytes::Bytes;
2323
use http::HeaderValue;
2424
use http::{header::AsHeaderName, HeaderMap};
25-
use log::error;
2625
use pingora_error::Result;
2726
use pingora_http::{RequestHeader, ResponseHeader};
2827
use std::time::Duration;
@@ -294,10 +293,23 @@ impl Session {
294293
}
295294
}
296295

297-
/// Send error response to client
298-
pub async fn respond_error(&mut self, error: u16) {
299-
let resp = Self::generate_error(error);
296+
/// Send error response to client using a pre-generated error message.
297+
pub async fn respond_error(&mut self, error: u16) -> Result<()> {
298+
self.respond_error_with_body(error, Bytes::default()).await
299+
}
300300

301+
/// Send error response to client using a pre-generated error message and custom body.
302+
pub async fn respond_error_with_body(&mut self, error: u16, body: Bytes) -> Result<()> {
303+
let mut resp = Self::generate_error(error);
304+
if !body.is_empty() {
305+
// error responses have a default content-length of zero
306+
resp.set_content_length(body.len())?
307+
}
308+
self.write_error_response(resp, body).await
309+
}
310+
311+
/// Send an error response to a client with a response header and body.
312+
pub async fn write_error_response(&mut self, resp: ResponseHeader, body: Bytes) -> Result<()> {
301313
// TODO: we shouldn't be closing downstream connections on internally generated errors
302314
// and possibly other upstream connect() errors (connection refused, timeout, etc)
303315
//
@@ -306,11 +318,12 @@ impl Session {
306318
// rather than a misleading the client with 'keep-alive'
307319
self.set_keepalive(None);
308320

309-
self.write_response_header(Box::new(resp))
310-
.await
311-
.unwrap_or_else(|e| {
312-
error!("failed to send error response to downstream: {e}");
313-
});
321+
self.write_response_header(Box::new(resp)).await?;
322+
if !body.is_empty() {
323+
self.write_response_body(body, true).await?;
324+
self.finish_body().await?;
325+
}
326+
Ok(())
314327
}
315328

316329
/// Whether there is no request body

pingora-core/src/protocols/http/v1/server.rs

+2-27
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use bytes::Bytes;
1818
use bytes::{BufMut, BytesMut};
1919
use http::HeaderValue;
2020
use http::{header, header::AsHeaderName, Method, Version};
21-
use log::{debug, error, warn};
21+
use log::{debug, warn};
2222
use once_cell::sync::Lazy;
2323
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
2424
use pingora_error::{Error, ErrorType::*, OrErr, Result};
@@ -30,7 +30,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
3030

3131
use super::body::{BodyReader, BodyWriter};
3232
use super::common::*;
33-
use crate::protocols::http::{body_buffer::FixedBuffer, date, error_resp, HttpTask};
33+
use crate::protocols::http::{body_buffer::FixedBuffer, date, HttpTask};
3434
use crate::protocols::{Digest, SocketAddr, Stream};
3535
use crate::utils::{BufRef, KVRef};
3636

@@ -895,31 +895,6 @@ impl HttpSession {
895895
}
896896
}
897897

898-
/// Return a error response to the client. This default error response comes with `cache-control: private, no-store`.
899-
/// It has no response body.
900-
pub async fn respond_error(&mut self, error_status_code: u16) {
901-
let (resp, resp_tmp) = match error_status_code {
902-
/* commmon error responses are pre-generated */
903-
502 => (Some(&*error_resp::HTTP_502_RESPONSE), None),
904-
400 => (Some(&*error_resp::HTTP_400_RESPONSE), None),
905-
_ => (
906-
None,
907-
Some(error_resp::gen_error_response(error_status_code)),
908-
),
909-
};
910-
911-
let resp = match resp {
912-
Some(r) => r,
913-
None => resp_tmp.as_ref().unwrap(),
914-
};
915-
916-
self.write_response_header_ref(resp)
917-
.await
918-
.unwrap_or_else(|e| {
919-
error!("failed to send error response to downstream: {}", e);
920-
});
921-
}
922-
923898
/// Write a `100 Continue` response to the client.
924899
pub async fn write_continue_response(&mut self) -> Result<()> {
925900
// only send if we haven't already

pingora-http/src/lib.rs

+19
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ impl ResponseHeader {
492492
pub fn as_owned_parts(&self) -> RespParts {
493493
clone_resp_parts(&self.base)
494494
}
495+
496+
/// Helper function to set the HTTP content length on the response header.
497+
pub fn set_content_length(&mut self, len: usize) -> Result<()> {
498+
self.insert_header(http::header::CONTENT_LENGTH, len)
499+
}
495500
}
496501

497502
fn clone_req_parts(me: &ReqParts) -> ReqParts {
@@ -754,4 +759,18 @@ mod tests {
754759
// Some(false)
755760
assert!(!req.send_end_stream().unwrap());
756761
}
762+
763+
#[test]
764+
fn set_test_set_content_length() {
765+
let mut resp = ResponseHeader::new(None);
766+
resp.set_content_length(10).unwrap();
767+
768+
assert_eq!(
769+
b"10",
770+
resp.headers
771+
.get(http::header::CONTENT_LENGTH)
772+
.map(|d| d.as_bytes())
773+
.unwrap()
774+
);
775+
}
757776
}

pingora-proxy/examples/gateway.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
use async_trait::async_trait;
16+
use bytes::Bytes;
1617
use clap::Parser;
1718
use log::info;
1819
use prometheus::register_int_counter;
@@ -42,7 +43,9 @@ impl ProxyHttp for MyGateway {
4243
if session.req_header().uri.path().starts_with("/login")
4344
&& !check_login(session.req_header())
4445
{
45-
let _ = session.respond_error(403).await;
46+
let _ = session
47+
.respond_error_with_body(403, Bytes::from_static(b"no way!"))
48+
.await;
4649
// true: early return as the response is already written
4750
return Ok(true);
4851
}
@@ -103,7 +106,7 @@ impl ProxyHttp for MyGateway {
103106
}
104107
}
105108

106-
// RUST_LOG=INFO cargo run --example load_balancer
109+
// RUST_LOG=INFO cargo run --example gateway
107110
// curl 127.0.0.1:6191 -H "Host: one.one.one.one"
108111
// curl 127.0.0.1:6190/family/ -H "Host: one.one.one.one"
109112
// curl 127.0.0.1:6191/login/ -H "Host: one.one.one.one" -I -H "Authorization: password"

pingora-proxy/src/lib.rs

+15-10
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,14 @@ impl<SV> HttpProxy<SV> {
143143
}
144144
Err(mut e) => {
145145
e.as_down();
146-
error!("Fail to proxy: {}", e);
146+
error!("Fail to proxy: {e}");
147147
if matches!(e.etype, InvalidHTTPHeader) {
148-
downstream_session.respond_error(400).await;
148+
downstream_session
149+
.respond_error(400)
150+
.await
151+
.unwrap_or_else(|e| {
152+
error!("failed to send error response to downstream: {e}");
153+
});
149154
} // otherwise the connection must be broken, no need to send anything
150155
downstream_session.shutdown().await;
151156
return None;
@@ -344,16 +349,16 @@ impl Session {
344349
&self.downstream_session
345350
}
346351

347-
/// Write HTTP response with the given error code to the downstream
352+
/// Write HTTP response with the given error code to the downstream.
348353
pub async fn respond_error(&mut self, error: u16) -> Result<()> {
349-
let resp = HttpSession::generate_error(error);
350-
self.write_response_header(Box::new(resp), true)
354+
self.as_downstream_mut().respond_error(error).await
355+
}
356+
357+
/// Write HTTP response with the given error code to the downstream with a body.
358+
pub async fn respond_error_with_body(&mut self, error: u16, body: Bytes) -> Result<()> {
359+
self.as_downstream_mut()
360+
.respond_error_with_body(error, body)
351361
.await
352-
.unwrap_or_else(|e| {
353-
self.downstream_session.set_keepalive(None);
354-
error!("failed to send error response to downstream: {e}");
355-
});
356-
Ok(())
357362
}
358363

359364
/// Write the given HTTP response header to the downstream

pingora-proxy/src/proxy_cache.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,13 @@ impl<SV> HttpProxy<SV> {
295295
}
296296
Err(e) => {
297297
// TODO: more logging and error handling
298-
session.as_mut().respond_error(500).await;
298+
session
299+
.as_mut()
300+
.respond_error(500)
301+
.await
302+
.unwrap_or_else(|e| {
303+
error!("failed to send error response to downstream: {e}");
304+
});
299305
// we have not write anything dirty to downstream, it is still reusable
300306
return (true, Some(e));
301307
}

pingora-proxy/src/proxy_trait.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ pub trait ProxyHttp {
372372
where
373373
Self::CTX: Send + Sync,
374374
{
375-
let server_session = session.as_mut();
376375
let code = match e.etype() {
377376
HTTPStatus(code) => *code,
378377
_ => {
@@ -392,7 +391,9 @@ pub trait ProxyHttp {
392391
}
393392
};
394393
if code > 0 {
395-
server_session.respond_error(code).await
394+
session.respond_error(code).await.unwrap_or_else(|e| {
395+
error!("failed to send error response to downstream: {e}");
396+
});
396397
}
397398
code
398399
}

0 commit comments

Comments
 (0)