Skip to content

Commit be97e35

Browse files
andrewhavckeaufavor
authored andcommitted
Add ability to ignore informational responses when proxying downstream
1 parent 999e379 commit be97e35

File tree

5 files changed

+145
-19
lines changed

5 files changed

+145
-19
lines changed

.bleep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9b88d76089e0f81c67cb502422148d4d26d4977e
1+
d112ca7464812ac6e575f46ec5d37b5da365dd82

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,19 @@ impl Session {
218218
}
219219
}
220220

221+
/// Sets whether we ignore writing informational responses downstream.
222+
///
223+
/// For HTTP/1.1 this is a noop if the response is Upgrade or Continue and
224+
/// Expect: 100-continue was set on the request.
225+
///
226+
/// This is a noop for h2 because informational responses are always ignored.
227+
pub fn set_ignore_info_resp(&mut self, ignore: bool) {
228+
match self {
229+
Self::H1(s) => s.set_ignore_info_resp(ignore),
230+
Self::H2(_) => {} // always ignored
231+
}
232+
}
233+
221234
/// Return a digest of the request including the method, path and Host header
222235
// TODO: make this use a `Formatter`
223236
pub fn request_summary(&self) -> String {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ pub(super) fn is_upgrade_req(req: &RequestHeader) -> bool {
153153
req.version == http::Version::HTTP_11 && req.headers.get(header::UPGRADE).is_some()
154154
}
155155

156+
pub(super) fn is_expect_continue_req(req: &RequestHeader) -> bool {
157+
req.version == http::Version::HTTP_11
158+
// https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1
159+
&& req.headers.get(header::EXPECT).map_or(false, |v| {
160+
v.as_bytes().eq_ignore_ascii_case(b"100-continue")
161+
})
162+
}
163+
156164
// Unlike the upgrade check on request, this function doesn't check the Upgrade or Connection header
157165
// because when seeing 101, we assume the server accepts to switch protocol.
158166
// In reality it is not common that some servers don't send all the required headers to establish

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

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ pub struct HttpSession {
7474
digest: Box<Digest>,
7575
/// Minimum send rate to the client
7676
min_send_rate: Option<usize>,
77+
/// When this is enabled informational response headers will not be proxied downstream
78+
ignore_info_resp: bool,
7779
}
7880

7981
impl HttpSession {
@@ -109,6 +111,7 @@ impl HttpSession {
109111
upgraded: false,
110112
digest,
111113
min_send_rate: None,
114+
ignore_info_resp: false,
112115
}
113116
}
114117

@@ -388,6 +391,11 @@ impl HttpSession {
388391
/// Write the response header to the client.
389392
/// This function can be called more than once to send 1xx informational headers excluding 101.
390393
pub async fn write_response_header(&mut self, mut header: Box<ResponseHeader>) -> Result<()> {
394+
if header.status.is_informational() && self.ignore_info_resp(header.status.into()) {
395+
debug!("ignoring informational headers");
396+
return Ok(());
397+
}
398+
391399
if let Some(resp) = self.response_written.as_ref() {
392400
if !resp.status.is_informational() || self.upgraded {
393401
warn!("Respond header is already sent, cannot send again");
@@ -409,7 +417,7 @@ impl HttpSession {
409417
header.insert_header(header::CONNECTION, connection_value)?;
410418
}
411419

412-
if header.status.as_u16() == 101 {
420+
if header.status == 101 {
413421
// make sure the connection is closed at the end when 101/upgrade is used
414422
self.set_keepalive(None);
415423
}
@@ -510,6 +518,18 @@ impl HttpSession {
510518
(None, None)
511519
}
512520

521+
fn ignore_info_resp(&self, status: u16) -> bool {
522+
// ignore informational response if ignore flag is set and it's not an Upgrade and Expect: 100-continue isn't set
523+
self.ignore_info_resp && status != 101 && !(status == 100 && self.is_expect_continue_req())
524+
}
525+
526+
fn is_expect_continue_req(&self) -> bool {
527+
match self.request_header.as_deref() {
528+
Some(req) => is_expect_continue_req(req),
529+
None => false,
530+
}
531+
}
532+
513533
fn is_connection_keepalive(&self) -> Option<bool> {
514534
is_buf_keepalive(self.get_header(header::CONNECTION))
515535
}
@@ -824,6 +844,14 @@ impl HttpSession {
824844
}
825845
}
826846

847+
/// Sets whether we ignore writing informational responses downstream.
848+
///
849+
/// This is a noop if the response is Upgrade or Continue and
850+
/// Expect: 100-continue was set on the request.
851+
pub fn set_ignore_info_resp(&mut self, ignore: bool) {
852+
self.ignore_info_resp = ignore;
853+
}
854+
827855
/// Return the [Digest] of the connection.
828856
pub fn digest(&self) -> &Digest {
829857
&self.digest
@@ -1472,6 +1500,75 @@ mod tests_stream {
14721500
.unwrap();
14731501
}
14741502

1503+
#[tokio::test]
1504+
async fn write_informational_ignored() {
1505+
let wire = b"HTTP/1.1 200 OK\r\nFoo: Bar\r\n\r\n";
1506+
let mock_io = Builder::new().write(wire).build();
1507+
let mut http_stream = HttpSession::new(Box::new(mock_io));
1508+
// ignore the 100 Continue
1509+
http_stream.ignore_info_resp = true;
1510+
let response_100 = ResponseHeader::build(StatusCode::CONTINUE, None).unwrap();
1511+
http_stream
1512+
.write_response_header_ref(&response_100)
1513+
.await
1514+
.unwrap();
1515+
let mut response_200 = ResponseHeader::build(StatusCode::OK, None).unwrap();
1516+
response_200.append_header("Foo", "Bar").unwrap();
1517+
http_stream.update_resp_headers = false;
1518+
http_stream
1519+
.write_response_header_ref(&response_200)
1520+
.await
1521+
.unwrap();
1522+
}
1523+
1524+
#[tokio::test]
1525+
async fn write_informational_100_not_ignored_if_expect_continue() {
1526+
let input = b"GET / HTTP/1.1\r\nExpect: 100-continue\r\n\r\n";
1527+
let output = b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nFoo: Bar\r\n\r\n";
1528+
1529+
let mock_io = Builder::new().read(&input[..]).write(output).build();
1530+
let mut http_stream = HttpSession::new(Box::new(mock_io));
1531+
http_stream.read_request().await.unwrap();
1532+
http_stream.ignore_info_resp = true;
1533+
// 100 Continue is not ignored due to Expect: 100-continue on request
1534+
let response_100 = ResponseHeader::build(StatusCode::CONTINUE, None).unwrap();
1535+
http_stream
1536+
.write_response_header_ref(&response_100)
1537+
.await
1538+
.unwrap();
1539+
let mut response_200 = ResponseHeader::build(StatusCode::OK, None).unwrap();
1540+
response_200.append_header("Foo", "Bar").unwrap();
1541+
http_stream.update_resp_headers = false;
1542+
http_stream
1543+
.write_response_header_ref(&response_200)
1544+
.await
1545+
.unwrap();
1546+
}
1547+
1548+
#[tokio::test]
1549+
async fn write_informational_1xx_ignored_if_expect_continue() {
1550+
let input = b"GET / HTTP/1.1\r\nExpect: 100-continue\r\n\r\n";
1551+
let output = b"HTTP/1.1 200 OK\r\nFoo: Bar\r\n\r\n";
1552+
1553+
let mock_io = Builder::new().read(&input[..]).write(output).build();
1554+
let mut http_stream = HttpSession::new(Box::new(mock_io));
1555+
http_stream.read_request().await.unwrap();
1556+
http_stream.ignore_info_resp = true;
1557+
// 102 Processing is ignored
1558+
let response_102 = ResponseHeader::build(StatusCode::PROCESSING, None).unwrap();
1559+
http_stream
1560+
.write_response_header_ref(&response_102)
1561+
.await
1562+
.unwrap();
1563+
let mut response_200 = ResponseHeader::build(StatusCode::OK, None).unwrap();
1564+
response_200.append_header("Foo", "Bar").unwrap();
1565+
http_stream.update_resp_headers = false;
1566+
http_stream
1567+
.write_response_header_ref(&response_200)
1568+
.await
1569+
.unwrap();
1570+
}
1571+
14751572
#[tokio::test]
14761573
async fn write_101_switching_protocol() {
14771574
let wire = b"HTTP/1.1 101 Switching Protocols\r\nFoo: Bar\r\n\r\n";

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -194,22 +194,21 @@ impl HttpSession {
194194
return Ok(());
195195
}
196196

197-
// FIXME: we should ignore 1xx header because send_response() can only be called once
198-
// https://github.com/hyperium/h2/issues/167
199-
200-
if let Some(resp) = self.response_written.as_ref() {
201-
if !resp.status.is_informational() {
202-
warn!("Respond header is already sent, cannot send again");
203-
return Ok(());
204-
}
197+
if header.status.is_informational() {
198+
// ignore informational response 1xx header because send_response() can only be called once
199+
// https://github.com/hyperium/h2/issues/167
200+
debug!("ignoring informational headers");
201+
return Ok(());
205202
}
206203

207-
// no need to add these headers to 1xx responses
208-
if !header.status.is_informational() {
209-
/* update headers */
210-
header.insert_header(header::DATE, get_cached_date())?;
204+
if self.response_written.as_ref().is_some() {
205+
warn!("Response header is already sent, cannot send again");
206+
return Ok(());
211207
}
212208

209+
/* update headers */
210+
header.insert_header(header::DATE, get_cached_date())?;
211+
213212
// remove other h1 hop headers that cannot be present in H2
214213
// https://httpwg.org/specs/rfc7540.html#n-connection-specific-header-fields
215214
header.remove_header(&header::TRANSFER_ENCODING);
@@ -486,7 +485,8 @@ mod test {
486485
expected_trailers.insert("test", HeaderValue::from_static("trailers"));
487486
let trailers = expected_trailers.clone();
488487

489-
tokio::spawn(async move {
488+
let mut handles = vec![];
489+
handles.push(tokio::spawn(async move {
490490
let (h2, connection) = h2::client::handshake(client).await.unwrap();
491491
tokio::spawn(async move {
492492
connection.await.unwrap();
@@ -510,7 +510,7 @@ mod test {
510510
assert_eq!(data, server_body);
511511
let resp_trailers = body.trailers().await.unwrap().unwrap();
512512
assert_eq!(resp_trailers, expected_trailers);
513-
});
513+
}));
514514

515515
let mut connection = handshake(Box::new(server), None).await.unwrap();
516516
let digest = Arc::new(Digest::default());
@@ -520,7 +520,7 @@ mod test {
520520
.unwrap()
521521
{
522522
let trailers = trailers.clone();
523-
tokio::spawn(async move {
523+
handles.push(tokio::spawn(async move {
524524
let req = http.req_header();
525525
assert_eq!(req.method, Method::GET);
526526
assert_eq!(req.uri, "https://www.example.com/");
@@ -545,7 +545,11 @@ mod test {
545545
}
546546

547547
let response_header = Box::new(ResponseHeader::build(200, None).unwrap());
548-
http.write_response_header(response_header, false).unwrap();
548+
assert!(http
549+
.write_response_header(response_header.clone(), false)
550+
.is_ok());
551+
// this write should be ignored otherwise we will error
552+
assert!(http.write_response_header(response_header, false).is_ok());
549553

550554
// test idling after response header is sent
551555
tokio::select! {
@@ -559,7 +563,11 @@ mod test {
559563

560564
http.write_trailers(trailers).unwrap();
561565
http.finish().unwrap();
562-
});
566+
}));
567+
}
568+
for handle in handles {
569+
// ensure no panics
570+
assert!(handle.await.is_ok());
563571
}
564572
}
565573
}

0 commit comments

Comments
 (0)