summaryrefslogtreecommitdiff
path: root/src/ui/assets.rs
Commit message (Collapse)AuthorAge
* Ask clients to avoid re-requesting immutable assets for 90 days.Owen Jacobson2025-12-02
| | | | | | | | Immutable assets include compiled script chunks, as well as stylesheets, fonts, and images baked into the SvelteKit app bundle. They do _not_ include the service worker, the PWA manifest, or the version and environment JSON files that SvelteKit uses to configure the application on startup. This complements Pilcrow's `If-None-Modified` support: if a browser asks but has a cached copy locally, we use the `If-Not-Modified` header to detect that and send back a response instructing the client to use that copy, saving bandwidth but not round trips. This change instructs clients not to even try asking, if they're willing to cache the response, and to instead satisfy those assets entirely from cache, saving round trips. `Duration::from_hours` was added in Rust 1.91, so this change also includes a minimum Rust version bump.
* Cache static assets using an etag header.Owen Jacobson2025-12-02
| | | | | | | | | | | | When Pilcrow returns static assets, it now sets an `ETag` header, derived from the content of the asset. This header will change if the asset changes. Conforming browsers _may_ cache the response, and make a conditional request with an `If-None-Match` header on future requests for the same asset. If we see that header, Pilcrow will check whether the loaded asset is the same as the one the browser already had, and skip the response with a 304 if appropriate. This cuts down on the number of times clients will load the same script files and the same assets from the server. Endpoints that route to the index after doing some logic got a pretty major cleanup. The tangled logic between the actual handler and the error type made them challening to follow, and there wasn't a clean way to pass the `If-None-Match` header through into the error for use when determining the final response. This version instead combines the negative cases with _success_, which produces the desired responses with much more straightforwards code. I've also opted to support `If-None-Match` for these endpoints, even though they do logically change if the underlying chat state changes - because the _response body_ does not change, and that's what the HTTP spec (and HTTP clients) care about in this context. They will, however, return the response in full for situations like Not Found. (I know it looks like these endpoints now _require_ the `If-None-Match` header. Trust me on this: they do not. The `headers` create cooks up an empty `If-None-Match` header if none is supplied, and that empty header differs from any non-empty ETag.)
* Define a generic "Failed" case for app-level errors (and a few others).Owen Jacobson2025-11-25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | We were previously exporting root causes from one layer of abstraction to the next. For example, anything that called into the database could cause an `sqlx::Error`, so anything that transitively called into that logic exported a `Database(sqlx::Error)` error variant of its own, using `From` to map errors from inner type to outer type. This had a couple of side effects. First, it required each layer of error handling to carry with it a `From` implementation unwrapping and rewrapping root causes from the next layer down. This was particularly apparent in the event and boot endpoints, which had separate error cases unique to crypto key processing errors solely because they happened to involve handling events that contained those keys. There were others, including the pervasive `Database(sqlx::Error)` error variants. Separately, none of the error variants introduced for this purpose were being used for anything other than printing to stderr. All the complexity of From impls and all the structure of the error types was being thrown away at top-level error handlers. This change replaces most of those error types with a generic `Failed` error. A `Failed` carries with it two pieces of information: a (boxed) underlying error, of any boxable `Error` type, and text meant to explain the context and cause of an error. Code which acts on errors can treat `Failed` as a catch-all case, while individually handling errors that signify important cases. Errors can be moved into or out of the `Failed` case by refactoring, as needed. The design of `Failed` is heavily motivated by [anyhow's `context` system][context] as a way for the programmer to capture immediate intention as an explanation for some underlying error. However, instead of accepting the full breadth of types that implement `Display`, a `Failed` can only carry strings as explanation. We don't need the generality at this time, and the implementation underlying it is pretty complex for what it does. [context]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.context This change also means that the full source chain for an error is now available to top-level error handlers, allowing more complete error messages. For example, starting `pilcrow` with an invalid network listen address produces Failed to bind to www.google.com:64209 Caused by: Can't assign requested address (os error 49) instead of the previous Error: Io(Os { code: 49, kind: AddrNotAvailable, message: "Can't assign requested address" }) which previously captured the same _cause_, but without the formatting (see previous commit) and without the _context_ (this commit). Similar improvements are available for many of the error scenarios Pilcrow is designed to give up on. When deciding which errors to use `Failed` with, I've used the heuristic that if something can fail for more than one underlying reason, and if the caller will only ever need to be able to differentiate those reasons after substantial refactoring anyways, then the reasons should collase into `Failed`. If there's either only a single underlying failure reason possible, or only errors arising out of the function body possible, then I've left error handling alone. In the process I've refactored most request-handler-level error mappings to explicitly map `Failed` to `Internal`, rather than having a catch-all mapping for all unhandled errors, to make it easier to remember to add request-level error representations when adding app-level error cases. This also includes helper traits for `Error` and `Result`, to make constructing `Failed` (and errors that include `Failed` as an alternative) easier to do, and some constants for the recurring error messages related to transaction demarcation. I'm not completely happy with the repetitive nature of those error cases, but this is the best I've arrived at so far. As errors are no longer expected to be convertible up the call stack, the `NotFound` and `Duplicate` helper traits for database errors had to change a bit. Those previously assumed that they would be used in the context of an error type implementing `From<sqlx::Error>` (or from another error type with similar characteristics), and that's not the case any more. The resulting idiom for converting a missing value into a domain error is `foo.await.optional().fail(MESSAGE)?.ok_or(DOMAIN ERROR)?`, which is rather clunky, but I've opted not to go further with it. The `Duplicate` helper is just plain gone, as it's not easily generalizable in this structure and using `match` is more tractable for me. Finally, I've changed the convention for error messages from `all lowercase messages in whatever tense i feel like at the moment` to `Sentence-case messages in the past tense`, frequently starting with `Failed to` and a short summary of the task at hand. This, as above, makes error message capitalization between Pilcrow's own messages and messages coming from other libraries/the Rust stdlib much more coherent and less jarring to read.
* Build the Sveltekit UI into Cargo's OUT_DIR.Owen Jacobson2025-05-28
| | | | | | | | | | | | | | | | This has a couple of material consequences: * It will be (much) easier to reorganize the source tree, as the path to the output is no longer relative to where the config files are when building the final binary. If we do decide to move `ui` into its own child crate, we won't have to make a bunch of (very similar) changes to the Svelte build process at that time. * There is less chance of a stale build contaminating a new one, since changes to the crate change the project hash in `OUT_DIR`. For example, while working on this change, `OUT_DIR` was at various points: * `target/debug/build/pilcrow-7cfeef3536ddd3e7/out` * `target/debug/build/pilcrow-09d4ddbc12bef36b/out` * `target/release/build/pilcrow-070d373bd5f850a1` This may use more space on disk, but it's all reclaimable with `cargo clean` and Rust is _far_ more profligate with disk space than Svelte will ever be. * It's more consistent with Cargo's expectations around generated source files, and thus potentially easier to onboard Rust developers into.
* Remove a bunch of clippy suppressions.Owen Jacobson2025-05-21
| | | | Notably, one of them was hiding a real (if unreachable) bug, by converting a "the token you have presented is not valid" scenario into an internal server error.
* Upgrade to Rust 1.85 and Rust 2024 edition.Owen Jacobson2025-02-20
| | | | | | | | There are a couple of migration suggestions from `cargo fix --edition` that I have deliberately skipped, which are intended to make sure that the changes to `if let` scoping don't bite us. They don't, I'm pretty sure, and if I turn out to be wrong, I'd rather fix the scoping issues (as they arise) than use `match` (`cargo fix --edition`'s suggestion). This change also includes a bulk reformat and a clippy cleanup. NOTA BENE: As this requires a new Rust toolchain, you'll need to update Rust (`rustup update`, normally) or the server won't build. This also applies to the Debian builder Docker image; it'll need to be rebuilt (from scratch, pulling its base image again) as well.
* Set `charset` params on returned content types.Owen Jacobson2024-10-22
| | | | This is a somewhat indirect change; it removes `mime_guess` in favour of some very, uh, "bespoke" mime detection logic that hardcodes mime types for the small repertoire of file extensions actually present in the UI. `mime_guess` doesn't provide a way to set params as it exports its own `Mime` struct, which doesn't provide `with_params()`.
* Organizational pass on endpoints and routes.Owen Jacobson2024-10-16