Skip to content
This repository was archived by the owner on Jun 21, 2024. It is now read-only.

Commit b5c4ebf

Browse files
authored
Merge branch 'main' into flag-boilerplate
2 parents 877e4df + e2ce466 commit b5c4ebf

File tree

9 files changed

+276
-67
lines changed

9 files changed

+276
-67
lines changed

.github/workflows/rust.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ jobs:
1717
- uses: actions/checkout@v3
1818

1919
- name: Install rust
20-
uses: dtolnay/rust-toolchain@master
21-
with:
22-
toolchain: stable
20+
uses: dtolnay/rust-toolchain@1.77
2321

2422
- uses: actions/cache@v3
2523
with:
@@ -52,9 +50,7 @@ jobs:
5250
echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts
5351
5452
- name: Install rust
55-
uses: dtolnay/rust-toolchain@master
56-
with:
57-
toolchain: stable
53+
uses: dtolnay/rust-toolchain@1.77
5854

5955
- uses: actions/cache@v3
6056
with:
@@ -73,10 +69,9 @@ jobs:
7369
steps:
7470
- uses: actions/checkout@v3
7571

76-
- name: Install latest rust
77-
uses: dtolnay/rust-toolchain@master
72+
- name: Install rust
73+
uses: dtolnay/rust-toolchain@1.77
7874
with:
79-
toolchain: stable
8075
components: clippy,rustfmt
8176

8277
- uses: actions/cache@v3

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ metrics = "0.22.0"
4747
metrics-exporter-prometheus = "0.14.0"
4848
rand = "0.8.5"
4949
rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl", "tracing"] }
50-
reqwest = { version = "0.12.3", features = ["json"] }
50+
reqwest = { version = "0.12.3", features = ["json", "stream"] }
5151
serde = { version = "1.0", features = ["derive"] }
5252
serde_derive = { version = "1.0" }
5353
serde_json = { version = "1.0" }

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.74.0-buster AS chef
1+
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.77-bookworm AS chef
22
ARG BIN
33
WORKDIR /app
44

@@ -20,7 +20,7 @@ RUN cargo chef cook --release --recipe-path recipe.json
2020
COPY . .
2121
RUN cargo build --release --bin $BIN
2222

23-
FROM debian:bullseye-20230320-slim AS runtime
23+
FROM debian:bookworm-slim AS runtime
2424

2525
RUN apt-get update && \
2626
apt-get install -y --no-install-recommends \

hook-worker/src/dns.rs

Whitespace-only changes.

hook-worker/src/error.rs

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,125 @@
1+
use std::fmt;
12
use std::time;
23

3-
use hook_common::pgqueue;
4+
use hook_common::{pgqueue, webhook::WebhookJobError};
45
use thiserror::Error;
56

6-
/// Enumeration of errors related to webhook job processing in the WebhookWorker.
7+
/// Enumeration of error classes handled by `WebhookWorker`.
78
#[derive(Error, Debug)]
89
pub enum WebhookError {
10+
#[error(transparent)]
11+
Parse(#[from] WebhookParseError),
12+
#[error(transparent)]
13+
Request(#[from] WebhookRequestError),
14+
}
15+
16+
/// Enumeration of parsing errors that can occur as `WebhookWorker` sets up a webhook.
17+
#[derive(Error, Debug)]
18+
pub enum WebhookParseError {
919
#[error("{0} is not a valid HttpMethod")]
1020
ParseHttpMethodError(String),
1121
#[error("error parsing webhook headers")]
1222
ParseHeadersError(http::Error),
1323
#[error("error parsing webhook url")]
1424
ParseUrlError(url::ParseError),
15-
#[error("a webhook could not be delivered but it could be retried later: {error}")]
25+
}
26+
27+
/// Enumeration of request errors that can occur as `WebhookWorker` sends a request.
28+
#[derive(Error, Debug)]
29+
pub enum WebhookRequestError {
1630
RetryableRequestError {
1731
error: reqwest::Error,
32+
response: Option<String>,
1833
retry_after: Option<time::Duration>,
1934
},
20-
#[error("a webhook could not be delivered and it cannot be retried further: {0}")]
21-
NonRetryableRetryableRequestError(reqwest::Error),
35+
NonRetryableRetryableRequestError {
36+
error: reqwest::Error,
37+
response: Option<String>,
38+
},
39+
}
40+
41+
/// Enumeration of errors that can occur while handling a `reqwest::Response`.
42+
/// Currently, not consumed anywhere. Grouped here to support a common error type for
43+
/// `utils::first_n_bytes_of_response`.
44+
#[derive(Error, Debug)]
45+
pub enum WebhookResponseError {
46+
#[error("failed to parse a response as UTF8")]
47+
ParseUTF8StringError(#[from] std::str::Utf8Error),
48+
#[error("error while iterating over response body chunks")]
49+
StreamIterationError(#[from] reqwest::Error),
50+
#[error("attempted to slice a chunk of length {0} with an out of bounds index of {1}")]
51+
ChunkOutOfBoundsError(usize, usize),
52+
}
53+
54+
/// Implement display of `WebhookRequestError` by appending to the underlying `reqwest::Error`
55+
/// any response message if available.
56+
impl fmt::Display for WebhookRequestError {
57+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58+
match self {
59+
WebhookRequestError::RetryableRequestError {
60+
error, response, ..
61+
}
62+
| WebhookRequestError::NonRetryableRetryableRequestError { error, response } => {
63+
let response_message = match response {
64+
Some(m) => m.to_string(),
65+
None => "No response from the server".to_string(),
66+
};
67+
writeln!(f, "{}", error)?;
68+
write!(f, "{}", response_message)?;
69+
70+
Ok(())
71+
}
72+
}
73+
}
74+
}
75+
76+
/// Implementation of `WebhookRequestError` designed to further describe the error.
77+
/// In particular, we pass some calls to underyling `reqwest::Error` to provide more details.
78+
impl WebhookRequestError {
79+
pub fn is_timeout(&self) -> bool {
80+
match self {
81+
WebhookRequestError::RetryableRequestError { error, .. }
82+
| WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => {
83+
error.is_timeout()
84+
}
85+
}
86+
}
87+
88+
pub fn is_status(&self) -> bool {
89+
match self {
90+
WebhookRequestError::RetryableRequestError { error, .. }
91+
| WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => {
92+
error.is_status()
93+
}
94+
}
95+
}
96+
97+
pub fn status(&self) -> Option<http::StatusCode> {
98+
match self {
99+
WebhookRequestError::RetryableRequestError { error, .. }
100+
| WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => {
101+
error.status()
102+
}
103+
}
104+
}
105+
}
106+
107+
impl From<&WebhookRequestError> for WebhookJobError {
108+
fn from(error: &WebhookRequestError) -> Self {
109+
if error.is_timeout() {
110+
WebhookJobError::new_timeout(&error.to_string())
111+
} else if error.is_status() {
112+
WebhookJobError::new_http_status(
113+
error.status().expect("status code is defined").into(),
114+
&error.to_string(),
115+
)
116+
} else {
117+
// Catch all other errors as `app_metrics::ErrorType::Connection` errors.
118+
// Not all of `reqwest::Error` may strictly be connection errors, so our supported error types may need an extension
119+
// depending on how strict error reporting has to be.
120+
WebhookJobError::new_connection(&error.to_string())
121+
}
122+
}
22123
}
23124

24125
/// Enumeration of errors related to initialization and consumption of webhook jobs.

hook-worker/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod config;
22
pub mod error;
3+
pub mod util;
34
pub mod worker;

hook-worker/src/util.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crate::error::WebhookResponseError;
2+
use futures::StreamExt;
3+
use reqwest::Response;
4+
5+
pub async fn first_n_bytes_of_response(
6+
response: Response,
7+
n: usize,
8+
) -> Result<String, WebhookResponseError> {
9+
let mut body = response.bytes_stream();
10+
let mut buffer = String::with_capacity(n);
11+
12+
while let Some(chunk) = body.next().await {
13+
if buffer.len() >= n {
14+
break;
15+
}
16+
17+
let chunk = chunk?;
18+
let chunk_str = std::str::from_utf8(&chunk)?;
19+
let upper_bound = std::cmp::min(n - buffer.len(), chunk_str.len());
20+
21+
if let Some(partial_chunk_str) = chunk_str.get(0..upper_bound) {
22+
buffer.push_str(partial_chunk_str);
23+
} else {
24+
// For whatever reason we are out of bounds. We should never land here
25+
// given the `std::cmp::min` usage, but I am being extra careful by not
26+
// using a slice index that would panic instead.
27+
return Err(WebhookResponseError::ChunkOutOfBoundsError(
28+
chunk_str.len(),
29+
upper_bound,
30+
));
31+
}
32+
}
33+
34+
Ok(buffer)
35+
}

0 commit comments

Comments
 (0)