Skip to content

Commit 206d2be

Browse files
committed
chore: refine comments, add logging for sqlx/anyhow errors
1 parent 718a89e commit 206d2be

File tree

7 files changed

+60
-17
lines changed

7 files changed

+60
-17
lines changed

.github/workflows/rust.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
uses: actions/checkout@v2
5050
with:
5151
repository: gothinkster/realworld
52+
# FIXME: we actually require this PR for the tests to pass: https://github.com/gothinkster/realworld/pull/490
5253
ref: a3855ae3a346e7bcb809dcddcc057bd13edc0c19
5354
path: realworld
5455
- name: Run Realworld Postman collection

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ sha2 = "0.9.8"
3434

3535
time = "0.2"
3636

37-
3837
uuid = { version = "0.8", features = ["serde"] }
3938

4039
# Utility Crates

src/http/articles/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ async fn create_article(
144144
// Never specified unless you count just showing them sorted in the examples:
145145
// https://realworld-docs.netlify.app/docs/specs/backend-specs/api-response-format#single-article
146146
//
147-
// However, it is required by the Postman collection.
147+
// However, it is required by the Postman collection. To their credit, the Realworld authors
148+
// have acknowledged this oversight and are willing to loosen the requirement:
149+
// https://github.com/gothinkster/realworld/issues/839#issuecomment-1002806224
148150
req.article.tag_list.sort();
149151

150152
// For fun, this is how we combine several operations into a single query for brevity.
@@ -424,7 +426,6 @@ async fn favorite_article(
424426
.await?
425427
.ok_or(Error::NotFound)?;
426428

427-
// It didn't really seem necessary to put this in a transaction.
428429
Ok(Json(ArticleBody {
429430
article: article_by_id(&ctx.db, auth_user.user_id, article_id).await?,
430431
}))
@@ -438,6 +439,8 @@ async fn unfavorite_article(
438439
) -> Result<Json<ArticleBody>> {
439440
// The Realworld spec doesn't say what to do if the user calls this on an article
440441
// that they haven't favorited. I've chosen to just do nothing as that's the easiest.
442+
//
443+
// The Postman collection doesn't test that case.
441444

442445
let article_id = sqlx::query_scalar!(
443446
r#"
@@ -458,7 +461,6 @@ async fn unfavorite_article(
458461
.await?
459462
.ok_or(Error::NotFound)?;
460463

461-
// It didn't really seem necessary to put this in a transaction.
462464
Ok(Json(ArticleBody {
463465
article: article_by_id(&ctx.db, auth_user.user_id, article_id).await?,
464466
}))
@@ -492,6 +494,10 @@ async fn get_tags(ctx: Extension<ApiContext>) -> Result<Json<TagsBody>> {
492494
// End handler functions.
493495
// Begin utility functions.
494496

497+
// This is used in a few different places so it makes sense to extract into its own function.
498+
//
499+
// I usually throw stuff like this at the bottom of the file but other engineers like
500+
// to put these kinds of functions in their own modules. Po-tay-to po-tah-to.
495501
async fn article_by_id(
496502
e: impl Executor<'_, Database = Postgres>,
497503
user_id: Uuid,
@@ -538,6 +544,8 @@ async fn article_by_id(
538544
/// Convert a title string to a slug for identifying an article.
539545
///
540546
/// E.g. `slugify("Doctests are the Bee's Knees") == "doctests-are-the-bees-knees"`
547+
///
548+
// (Sadly, doctests are not run on private functions it seems.)
541549
fn slugify(string: &str) -> String {
542550
const QUOTE_CHARS: &[char] = &['\'', '"'];
543551

src/http/error.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ pub enum Error {
3636
///
3737
/// For a good API, the other status codes should also ideally map to some sort of JSON body
3838
/// that the frontend can deal with, but I do admit sometimes I've just gotten lazy and
39-
/// returned a plain error message if I can get away with just using distinct status codes
40-
/// without deviating too much from their specifications.
39+
/// returned a plain error message if there were few enough error modes for a route
40+
/// that the frontend could infer the error from the status code alone.
4141
#[error("error in the request body")]
4242
UnprocessableEntity {
4343
errors: HashMap<Cow<'static, str>, Vec<Cow<'static, str>>>,
@@ -48,12 +48,15 @@ pub enum Error {
4848
/// Via the generated `From<sqlx::Error> for Error` impl,
4949
/// this allows using `?` on database calls in handler functions without a manual mapping step.
5050
///
51+
/// I highly recommend creating an error type like this if only to make handler function code
52+
/// nicer; code in Actix-web projects that we started before I settled on this pattern is
53+
/// filled with `.map_err(ErrInternalServerError)?` which is a *ton* of unnecessary noise.
54+
///
5155
/// The actual error message isn't returned to the client for security reasons.
5256
/// It should be logged instead.
5357
///
5458
/// Note that this could also contain database constraint errors, which should usually
55-
/// be transformed into client errors (e.g. `422 Unprocessable Entity`).
56-
///
59+
/// be transformed into client errors (e.g. `422 Unprocessable Entity` or `409 Conflict`).
5760
/// See `ResultExt` below for a convenient way to do this.
5861
#[error("an error occurred with the database")]
5962
Sqlx(#[from] sqlx::Error),
@@ -62,7 +65,8 @@ pub enum Error {
6265
///
6366
/// `anyhow::Error` is used in a few places to capture context and backtraces
6467
/// on unrecoverable (but technically non-fatal) errors which could be highly useful for
65-
/// debugging.
68+
/// debugging. We use it a lot in our code for background tasks or making API calls
69+
/// to external services so we can use `.context()` to refine the logged error.
6670
///
6771
/// Via the generated `From<anyhow::Error> for Error` impl, this allows the
6872
/// use of `?` in handler functions to automatically convert `anyhow::Error` into a response.
@@ -124,7 +128,7 @@ impl IntoResponse for Error {
124128
errors: HashMap<Cow<'static, str>, Vec<Cow<'static, str>>>,
125129
}
126130

127-
(StatusCode::UNPROCESSABLE_ENTITY, Json(Errors { errors })).into_response()
131+
return (StatusCode::UNPROCESSABLE_ENTITY, Json(Errors { errors })).into_response();
128132
}
129133
Self::Unauthorized => (
130134
self.status_code(),
@@ -137,17 +141,30 @@ impl IntoResponse for Error {
137141
//
138142
// However, at Launchbadge we try to adhere to web standards wherever possible,
139143
// if nothing else than to try to act as a vanguard of sanity on the web.
140-
//
141-
// "Your scientists were so preoccupied with whether or not they *could*,
142-
// they didn't stop to think if they *should*."
143144
[(WWW_AUTHENTICATE, HeaderValue::from_static("Token"))]
144145
.into_iter()
145146
.collect::<HeaderMap>(),
146147
self.to_string(),
147148
)
148149
.into_response(),
149-
other => (other.status_code(), other.to_string()).into_response(),
150+
151+
Self::Sqlx(e) => {
152+
// TODO: we probably want to use `tracing` instead
153+
// so that this gets linked to the HTTP request by `TraceLayer`.
154+
log::error!("SQLx error: {:?}", e);
155+
}
156+
157+
Self::Anyhow(e) => {
158+
// TODO: we probably want to use `tracing` instead
159+
// so that this gets linked to the HTTP request by `TraceLayer`.
160+
log::error!("Generic error: {:?}", e);
161+
}
162+
163+
// Other errors get mapped normally.
164+
_ => (),
150165
}
166+
167+
(self.status_code(), self.to_string()).into_response()
151168
}
152169
}
153170

src/http/profiles.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async fn follow_user(
8989
// because you were too clever. You can always improve performance later if the
9090
// implementation proves to be a bottleneck.
9191
//
92-
// Readability is also tantamount if you need to onboard more devs to the project.
92+
// Readability is also paramount if you need to onboard more devs to the project.
9393
//
9494
// Trust me, I've learned this the hard way.
9595

src/http/types.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ use time::{Format, OffsetDateTime};
55

66
/// `OffsetDateTime` provides RFC-3339 (ISO-8601 subset) serialization, but the default
77
/// `serde::Serialize` implementation produces array of integers, which is great for binary
8-
/// serialization but very difficult to consume when returned from an API.
8+
/// serialization, but infeasible to consume when returned from an API, and certainly
9+
/// not human-readable.
910
///
1011
/// With this wrapper type, we override this to provide the serialization format we want.
12+
///
13+
/// `chrono::DateTime` doesn't need this treatment, but Chrono sadly seems to have stagnated,
14+
/// and has a few more papercuts than I'd like:
15+
///
16+
/// * Having to import both `DateTime` and `Utc` everywhere gets annoying quickly.
17+
/// * lack of `const fn` constructors anywhere (especially for `chrono::Duration`)
18+
/// * `cookie::CookieBuilder` (used by Actix-web and `tower-cookies`) bakes-in `time::Duration`
19+
/// for setting the expiration
20+
/// * not really Chrono's fault but certainly doesn't help.
1121
#[derive(sqlx::Type)]
1222
pub struct Timestamptz(pub OffsetDateTime);
1323

@@ -31,6 +41,14 @@ impl<'de> Deserialize<'de> for Timestamptz {
3141
//
3242
// We could deserialize a borrowed `&str` directly but certain deserialization modes
3343
// of `serde_json` don't support that, so we'd be forced to always deserialize `String`.
44+
//
45+
// `serde_with` has a helper for this but it can be a bit overkill to bring in
46+
// just for one type: https://docs.rs/serde_with/latest/serde_with/#displayfromstr
47+
//
48+
// We'd still need to implement `Display` and `FromStr`, but those are much simpler
49+
// to work with.
50+
//
51+
// However, I also wanted to demonstrate that it was possible to do this with Serde alone.
3452
impl Visitor<'_> for StrVisitor {
3553
type Value = Timestamptz;
3654

src/http/users.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async fn create_user(
6363
) -> Result<Json<UserBody<User>>> {
6464
let password_hash = hash_password(req.user.password).await?;
6565

66-
// We like using queries inline in our request handlers as it's easier to understand the
66+
// I personally prefer using queries inline in request handlers as it's easier to understand the
6767
// query's semantics in the wider context of where it's invoked.
6868
//
6969
// Sometimes queries just get too darn big, though. In that case it may be a good idea

0 commit comments

Comments
 (0)