Tahinli/forge
publicTopcoat 0.10 API cheat-sheet (git forge web layer)
REG = /home/tahinli/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f. Sub-crates: REG/topcoat-<name>-0.10.0/. UNVERIFIED = not confirmed in source. Snippets checked against signatures and docs, not compiled.
Key files: facade REG/topcoat-0.10.0/src/{lib,serve}.rs; router REG/topcoat-router-0.10.0/src/{router,builder,request,response,error,body,body_limit,compression,origin,proxy,listener}.rs, .../src/content/{json,form}.rs; cookies REG/topcoat-cookie-0.10.0/src/{lib,prefix,store}.rs; context REG/topcoat-core-0.10.0/src/context/app_context.rs; views REG/topcoat-view-0.10.0/src/, REG/topcoat-view-macro-0.10.0/docs/view.md; guides REG/topcoat-0.10.0/docs/*.md, REG/topcoat-router-0.10.0/docs/*.md.
1. Feature flags
Default: asset, compression, cookie, font, icon, router, runtime, serve, session, view, discover.
| Feature | Enables |
|---|---|
router |
topcoat::router (routes, pages, layouts, layers, Router); pulls view |
serve |
topcoat::start/serve/serve_until. Without it use Router::handle only |
discover |
Link-time collection: module_router!(), .discover() |
view |
view!, #[component], class!, suspense, error_boundary |
cookie |
topcoat::cookie. Still call .cookies() on the builder |
asset |
asset!, AssetBundle |
compression |
Response compression (on by default, see 7e) |
session |
topcoat::session (UNVERIFIED, not covered) |
fs |
public_dir, serve_dir (not default) |
multipart |
content::Multipart (not default) |
websocket, sse, sitemap, tower, tailwind, datastar, htmx, alpine-ajax, mail, ui, font-fontsource, icon-iconify, anyhow, full |
Optional extras (not default) |
Minimal deps: topcoat = "0.10.0", tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util"] }. For streaming (4b) also http-body = "1", http-body-util = "0.1", tokio-util = { version = "0.7", features = ["io"] }, futures-util = "0.3". |
2. Routes
2a. Module routing (recommended)
// src/app.rs (module maps to "/")
use topcoat::router::{Router, module_router};
pub fn router() -> Router { module_router!().discover().build() }
module_router!()returns aRouterBuilder..discover()(traittopcoat::router::RouterBuilderDiscoverExt) also collects explicit-path items.app::about->/about;app::blog_posts->/blog-posts. Handler names do not affect the URL.- Without a path string,
#[page],#[route(GET)],#[layout],#[layer]take the module path.#[route(GET "./health")]appends to it. Source:REG/topcoat-router-0.10.0/docs/module.md.
2b. Explicit paths
use topcoat::{Result, router::{Router, page, route}, view::{View, view}};
#[page("/")]
async fn home() -> Result<impl View> { Ok(view! { <h1>"Home"</h1> }) }
#[route(GET "/api/health")]
async fn health() -> Result<&'static str> { Ok("ok") }
pub fn router() -> Router { Router::builder().page(home).route(health).build() }
Builder methods: .page(f), .route(f), .layout(f), .layer(f), .discover(), .build(). Source: REG/topcoat-router-0.10.0/src/builder.rs.
2c. Attributes
#[route(GET "/x")] // one method
#[route([GET, POST] "/x")] // several
#[route(* "/x")] // any method; specific-method routes win over `*`
#[page] // GET, module path
#[page("/x")] // GET, explicit
#[page(POST "/x")] // POST page
Route handlers return Result<T> where T: AsyncIntoResponse. Pages return Result<impl View>. Parameters: optional cx: &Cx and at most one body extractor, in either order. Overlapping methods at the same path fail at .build().
2d. Path params: /{owner}/{repo}/info/refs
use topcoat::{Result, context::Cx, router::{path_param, route}, view::{View, view}};
path_param!(owner); // unparsed: path_param::<Owner>(cx) -> &str (percent-decoded)
path_param!(repo);
path_param!(id: u64); // parsed with FromStr: path_param::<Id>(cx) -> Result<&u64, _>
path_param!(id2: u64, error = not_found); // error = not_found|bad_request|unauthorized|forbidden|redirect("/x")
path_param!(*rest); // catch-all: "{*rest}", path_param::<Rest>(cx) -> CatchAllSegments
#[route(GET "/{owner}/{repo}/info/refs")]
async fn info_refs(cx: &Cx) -> Result<&'static str> {
let owner: &str = path_param::<Owner>(cx);
let repo: &str = path_param::<Repo>(cx);
let _ = (owner, repo);
Ok("ok")
}
path_param!(name)generatesName(Pascal-cased). Name the parameter in the path as{name}. Reading a parameter the route did not capture panics.- Module form:
module_param!(post_id: u64, error = bad_request);insrc/app/posts/post_id.rsserves/posts/{post_id}(the module adds one segment).module_param!(*path)for catch-all. - Source:
REG/topcoat-router-macro-0.10.0/docs/path_param.md,.../docs/module_param.md.
2e. Query params
use topcoat::{Result, context::Cx, router::{page, query_params}, view::{View, view}};
#[query_params(error = bad_request)]
struct InfoRefsQuery { service: Option<String> }
#[page]
async fn page(cx: &Cx) -> Result<impl View> {
let q = query_params::<InfoRefsQuery>(cx)?; // &InfoRefsQuery, memoized
Ok(view! { <p>(q.service.as_deref().unwrap_or(""))</p> })
}
Derives serde::Deserialize. Works from any handler with cx. Source: REG/topcoat-router-macro-0.10.0/docs/query_params.md.
2f. Registration summary
module_router!()(module-derived),Router::builder()(manual),.discover()(all explicit-path items),.public_dir("./public")/.serve_dir("/p/{*f}", "./dir")(featurefs, static files).
3. Request access
Import helpers from topcoat::router::request. All take cx: &Cx.
| Need | Call | Returns |
|---|---|---|
| method | method(cx) |
&http::Method |
| uri / query | uri(cx) |
&http::Uri (uri(cx).query()) |
| headers | headers(cx) |
&HeaderMap |
| content type | content_type(cx) |
Option<&str> |
| all parts | parts(cx) |
&http::request::Parts |
| client IP | client_ip(cx) |
Option<IpAddr> (proxy-aware) |
| peer addr | remote_addr(cx) |
Option<SocketAddr> (None on Unix sockets) |
| pre-rewrite | original_uri(cx), original_headers(cx), ... |
same types |
client_ip returns the direct peer unless .trusted_proxies(TrustedProxies::new()...) is set on the builder. ForwardedHeader variant names: UNVERIFIED (REG/topcoat-router-0.10.0/src/proxy.rs).
Body extractors (one per handler):
| Type | Behavior |
|---|---|
Body (topcoat::router::Body) |
Streaming. Not limited. body.into_data_stream() yields Result<Bytes, BoxError> |
Bytes (topcoat::router::request::Bytes) |
Buffered, body limit applies (413) |
String |
Buffered UTF-8, body limit applies |
Json<T> (topcoat::router::content::Json) |
Requires Content-Type: application/json (400 otherwise); malformed = 400 |
Form<T> (topcoat::router::content::Form) |
x-www-form-urlencoded. On GET/HEAD reads the query string |
RawForm |
Raw form bytes |
Multipart (feature multipart) |
UNVERIFIED field API (REG/topcoat-router-0.10.0/docs/content/multipart.md) |
Option<T> |
Wraps any extractor to accept an absent body |
use topcoat::{Result, context::Cx, router::{Body, route, content::Form}};
#[route(POST "/{owner}/{repo}/git-receive-pack")]
async fn receive(cx: &Cx, body: Body) -> Result<&'static str> { let _ = (cx, body); Ok("") }
#[derive(serde::Deserialize)] struct NewRepo { name: String }
#[route(POST "/repos")]
async fn create(Form(input): Form<NewRepo>) -> Result<&'static str> { let _ = input.name; Ok("created") }
Custom extractor: impl FromRequest { async fn from_request(cx: &Cx, body: Body) -> Result<Self> }. Delegate buffering to Bytes or to_bytes(body, body_limit(cx)).
4. Responses and errors
4a. Return types
&'static str,String,Vec<u8>,Bytes,()-> 200 with body.StatusCode-> empty body.Body-> as-is.Json<T>-> JSON.Response(http::Response<Body>) -> full control.- Tuples: a leading
StatusCode,Parts, orResponse<()>sets the status; middle items areIntoResponseParts(HeaderMap,[(K, V); N],Extensions,Option<T>); the last item is the body. Source:REG/topcoat-router-0.10.0/src/response.rs.
use topcoat::{Result, router::{Body, HeaderMap, StatusCode, header, route}};
#[route(GET "/{owner}/{repo}/info/refs")]
async fn info_refs() -> Result<(StatusCode, HeaderMap, Body)> {
let mut h = HeaderMap::new();
h.insert(header::CONTENT_TYPE, "application/x-git-upload-pack-advertisement".parse().unwrap());
Ok((StatusCode::OK, h, Body::from("001e# service=git-upload-pack\n0000")))
}
// or: Ok((StatusCode::OK, [(header::CONTENT_TYPE, "text/plain")], Body::from("ok")))
4b. Streaming from a child process
Pattern from REG/topcoat-router-0.10.0/src/route/directory.rs (fn stream): Body::new(StreamBody::new(stream_of_frames)).
use std::process::Stdio;
use futures_util::TryStreamExt;
use http_body::Frame;
use http_body_util::StreamBody;
use tokio::process::Command;
use tokio_util::io::ReaderStream;
use topcoat::{Result, router::{Body, HeaderMap, StatusCode}};
async fn stream_child(mut cmd: Command) -> Result<(StatusCode, HeaderMap, Body)> {
let mut child = cmd.stdout(Stdio::piped()).spawn()?; // io::Error -> 500
let stdout = child.stdout.take().expect("piped");
let chunks = ReaderStream::new(stdout).map_ok(Frame::<bytes::Bytes>::data);
Ok((StatusCode::OK, HeaderMap::new(), Body::new(StreamBody::new(chunks))))
}
bytesis re-exported astopcoat::router::request::Bytes.- Caveat:
childdrops when this function returns. Withkill_on_drop(true)the process would die immediately, so either omit it or move theChildinto the stream (a struct owningchildand the stream). Withoutkill_on_drop, the child is not killed on client disconnect; it exits on SIGPIPE. UNVERIFIED that the router does anything here. - Request body to child stdin:
tokio_util::io::StreamReader::new(body.into_data_stream().map_err(std::io::Error::other))thentokio::io::copy.
4c. Errors -> status codes
topcoat::Result<T> = Result<T, topcoat_core::error::Error>. Any std::error::Error converts with ?. Non-router errors become 500 (body internal server error). Handler panics also become 500 (REG/topcoat-router-0.10.0/src/router.rs, handle).
Constructor (topcoat::router::error) |
Status | Ext on Option/Result |
|---|---|---|
not_found() |
404 | ok_or_not_found() |
bad_request("msg") |
400 | ok_or_bad_request(..) |
unauthorized() |
401 | ok_or_unauthorized() |
forbidden() |
403 | ok_or_forbidden() |
method_not_allowed(methods) |
405 (+ Allow) |
- |
content_too_large() |
413 | - |
too_many_requests(secs) |
429 | - |
service_unavailable(secs) |
503 | - |
redirect(uri) |
307 | ok_or_redirect(uri) |
redirect_permanent(uri) |
308 | ok_or_redirect_permanent(uri) |
see_other(uri) |
303 (SeeOther, usable as Ok(..)) |
- |
internal_server_error(e) |
500 | - |
use topcoat::{Result, context::Cx, router::{error::{RouterErrorExt, forbidden, see_other, SeeOther}, route}};
#[route(POST "/repos")]
async fn create(cx: &Cx) -> Result<SeeOther> { let _ = cx; Ok(see_other("/repos")) }
fn lookup() -> Result<&'static str> { Ok(None::<&str>.ok_or_not_found()?) } // 404
fn gate() -> Result<()> { Err(forbidden().into()) } // 403
Error boundary: a layout can wrap slot in error_boundary(fallback: |error| ..., (slot)) and error.downcast_ref::<NotFoundError>() for a branded 404 (REG/topcoat-0.10.0/docs/router.md).
Headers that must appear on error responses: topcoat::router::response::response_headers(cx).append(name, value) in a layer (REG/topcoat-router-0.10.0/src/response/headers.rs).
5. Views, components, layouts, escaping, forms
use topcoat::{Result, router::{Slot, layout}, view::{View, component, view, class}};
#[component]
async fn badge(label: &str, ok: bool) -> Result<impl View> {
Ok(view! { <span class=(class!("badge", "badge-ok" if ok))>(label)</span> })
}
#[layout] // or #[layout("/settings")]
async fn root(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
<h1>"Repo"</h1>
badge(label: "New", ok: true) // component call, named args
(slot) // inner page
</body>
</html>
})
}
#[component]
async fn list(items: Vec<String>) -> Result<impl View> {
Ok(view! {
<ul>
for item in items.iter() { <li>(item)</li> }
</ul>
})
}
- Syntax: HTML tags with real names; void tags without closing; text in quotes; Rust expressions in parentheses (child and attribute position);
if,for,match,letin markup; dynamic names<(tag) (attr)="x">; attribute names may contain-,:,.. Source:REG/topcoat-view-macro-0.10.0/docs/view.md,.../docs/component.md. - Components take typed params;
Slotfor layouts;cx: &Cxoptional, any order. - Escaping: yes, auto-escaped. Text and
(expr)interpolations escape<,&, etc., and attribute values are escaped too (tests inREG/topcoat-view-0.10.0/src/buffer/handle.rs,.../src/html/attribute.rs). Opt-out ispush_*_unescapedand anUnescaped<T>wrapper (REG/topcoat-view-0.10.0/src/string.rs); facade path UNVERIFIED. Do not wrap commit messages, file names, or diffs in it. - Links and forms:
href!(handler_fn)builds URLs from the handler's registered path;href!(mod::handler, mod::Param(1))passes params.<form method="post" action=(href!(publish))>from source docs. HTML forms only send GET/POST. Source:REG/topcoat-router-0.10.0/docs/href.md. - Active nav:
href.is_current(cx)plusclass!. - A
#[page]sets status or headers with aStatusCodeorHeaderMapelement insideview!(perREG/topcoat-0.10.0/docs/router.md; not run). For git endpoints use#[route]returningResponseor a tuple.
6. Cookies
use topcoat::{cookie::RouterBuilderCookieExt, router::{Router, RouterBuilderDiscoverExt, module_router}};
pub fn router() -> Router { module_router!().discover().cookies().build() } // required
use topcoat::{Result, context::Cx, cookie::{Cookie, Cookies, cookies}, router::route};
#[route(POST "/theme")]
async fn set_theme(cx: &Cx) -> Result<&'static str> {
let jar = cookies(cx); // &CookieJar; `Cookies` trait in scope
let _current = jar.get("theme"); // includes pending writes
jar.add(Cookie::build(("theme", "dark")).path("/").http_only(true).secure(true).build());
jar.remove(Cookie::build(("theme", "")).path("/")); // same Path/Domain as when set
Ok("ok")
}
add/removeacceptInto<Cookie>, sojar.add(("k", "v"))works.- Attributes:
Path,Domain,HttpOnly,Secure,SameSite,MaxAge, set via builder orcookie!("s" = "v"; Path = "/"; Secure; HttpOnly; SameSite = Lax)(topcoat::cookie::cookie). - Defaults/overrides on the jar:
cookies(cx).default_secure(true).default_http_only(true).default_same_site(SameSite::Lax).default_path("/"). Theoverride_*form forces a value. Wrap this in one helperfn cookies(cx) -> impl Cookies. __Host-: works.jar.override_prefix_host()stores__Host-name, forcesSecure,Path=/, noDomain; reads strip the prefix.override_prefix_secure()gives__Secure-. Browsers rejectSecurecookies over plainhttp://except on localhost (UNVERIFIED for your deployment).- Signed/private:
signed_cookies(cx),private_cookies(cx)need aKeyin app context:.app_context(Key::generate()). Calling them without a key panics. Persist the key across restarts.cookies(cx).private(&key)takes it explicitly. - Typed store:
cookie_store::<T, _>(private_cookies(cx), "cart").parse_or_default().update(|t| ..).commit()?. Nothing is written untilcommit(). - Timing: writes must happen before the handler returns. After the jar is sealed (e.g. inside a streaming body) writes panic. Reads still work.
- Source:
REG/topcoat-0.10.0/docs/cookie.md,REG/topcoat-cookie-0.10.0/src/{lib,prefix,store}.rs.
7. Layers, body limit, timeouts, compression, origin
7a. Layers
use topcoat::{Result, context::Cx, router::{Body, Next, layer, response::Response}};
#[layer("/api")] // no path = wraps every request
async fn timing(cx: &Cx, body: Body, next: Next<'_>) -> Result<Response> {
next.run(cx, body).await
}
// register: Router::builder().layer(timing)
- Matching is by path prefix; outer = least specific. Same-path layers from
module_router!are rejected; use explicit.layer()for ordering. A path-less layer also sees 404/405 asErrfromnext.run. Per-request values:let cx = cx.with(value). - Source:
REG/topcoat-router-0.10.0/docs/router.md,.../docs/layer.md.
7b. Tower (feature tower)
TowerLayer::new(mw).at("/api"), TowerRoute::any("/legacy/{*rest}", svc). Example: TowerLayer::new(tower::timeout::TimeoutLayer::new(Duration::from_secs(5))).at("/api"). Source: REG/topcoat-router-0.10.0/docs/tower.md.
7c. Body limit
- Default 2 MiB (
REG/topcoat-router-0.10.0/src/body_limit.rs), enforced byBytes,String,Json,Form. Over it: 413.Bodyis not limited. - Override per path:
use topcoat::router::{BodyLimit, module_router};
let router = module_router!()
.layer(BodyLimit::max(64 << 20).at("/upload"))
.layer(BodyLimit::disable().at("/{owner}/{repo}/git-receive-pack")) // `{param}` in .at(): UNVERIFIED
.build();
body_limit(cx)returns the effective limit for custom extractors.
7d. Timeouts
- No request, header, or idle timeout found in router or serve code (grepped
REG/topcoat-router-0.10.0/srcandREG/topcoat-0.10.0/src/serve.rs). - Only timeout: graceful shutdown,
RouterService::new(router).shutdown_timeout(d), default 30 s. Request timeout, if wanted: TowerLayer withTimeoutLayer(7b).
7e. Compression (on by default; matters for git)
- Negotiated from
Accept-Encodingafter all layers (REG/topcoat-router-0.10.0/src/compression.rs). - Streaming bodies of unknown size are always compressed. Excluded types: gRPC, images, SSE.
application/x-git-*-resultis not excluded, so pack responses would be compressed. - Fix (applied):
Router::builder().compression(Compression::off())on the public router, pluscrates/forge-web/src/compression.rs(ResponseCompression, a tower layer). Topcoat has no per-route or content-type switch, so the policy is in that layer. See architecture.md 5.6.
7f. Origin policy (CSRF guard, default on)
- Before layers run: state-changing cross-origin browser requests and cross-origin WebSocket handshakes get 403. Requests with no Origin or Fetch-Metadata headers pass, as do same-origin and direct navigations.
- Configure:
.origin_policy(OriginPolicy::new().trust_origins([...]))(REG/topcoat-router-0.10.0/src/origin.rs). - Git clients: expected to pass (no browser headers). UNVERIFIED per client.
8. Shared application state (answer to the critical question)
Shared state uses app context: values keyed by Rust type, registered once on the builder. Not an extension, and not a field on Cx.
// src/state.rs
pub struct AppState { pub repos_root: std::path::PathBuf, pub db: std::sync::Arc<Db> }
// router setup
pub fn router(state: AppState) -> topcoat::router::Router {
topcoat::router::module_router!().discover().app_context(state).build()
}
// any handler/layout/component with cx
use topcoat::context::{Cx, app_context, try_app_context};
pub fn state(cx: &Cx) -> &AppState { app_context::<AppState>(cx) } // panics if missing
pub fn maybe_state(cx: &Cx) -> Option<&AppState> { try_app_context(cx) }
- Bounds:
T: Any + Send + Sync + 'static. Duplicate type panics at registration. - Use
Arcinside for mutable resources (pools, locks). - Per-request values:
request_context::<T>(cx)/try_request_context, added by a layer withcx.with(value). - Background work:
let cx = cx.clone()and move it intotokio::spawn. A clonedCxstill reads app context, but cannot set response headers. - Source:
REG/topcoat-0.10.0/docs/context.md,REG/topcoat-router-0.10.0/src/builder.rs(app_context),REG/topcoat-core-0.10.0/src/context/app_context.rs.
9. Serving
topcoat::start(service: impl Into<RouterService>) -> io::Result<()> // HOST (127.0.0.1) / PORT (3000)
topcoat::serve(listener: impl Listener, service) -> io::Result<()> // Ctrl+C / SIGTERM
topcoat::serve_until(listener, service, signal: impl Future<Output = ()>) -> io::Result<()>
Source: REG/topcoat-0.10.0/src/serve.rs. Listener is implemented for tokio::net::TcpListener and tokio::net::UnixListener (REG/topcoat-router-0.10.0/src/listener.rs). Router converts to RouterService via From.
Two routers (public TCP, internal Unix socket) with one shared shutdown:
use tokio::net::{TcpListener, UnixListener};
use topcoat::router::RouterService;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let _ = std::fs::remove_file("/run/forge-internal.sock"); // stale socket is not removed for you
let tcp = TcpListener::bind("0.0.0.0:8080").await?;
let unix = UnixListener::bind("/run/forge-internal.sock")?;
let (stop, _) = tokio::sync::watch::channel(());
let (mut a, mut b) = (stop.subscribe(), stop.subscribe());
tokio::try_join!(
topcoat::serve_until(tcp, RouterService::new(public::router(state())), async move { let _ = a.changed().await; }),
topcoat::serve_until(unix, RouterService::new(internal::router(state())), async move { let _ = b.changed().await; }),
async { tokio::signal::ctrl_c().await?; stop.send_replace(()); Ok::<(), std::io::Error>(()) },
)?;
Ok(())
}
serveandstarteach install their own signal handlers, so useserve_untilfor several listeners. Unix listener:remote_addrandclient_ipreturnNone. Placeholders (public::router,state()) are not compiled.
10. Assets and CSS
use topcoat::{Result, asset::{Asset, asset}, router::{Slot, layout}, view::{View, view}};
const APP_CSS: Asset = asset!("assets/app.css"); // relative to CARGO_MANIFEST_DIR; "./x" is relative to the source file
#[layout]
async fn root(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html><head><link rel="stylesheet" href=(APP_CSS)></head><body>(slot)</body></html>
})
}
- Renders as
/_topcoat/assets/app-<hash>.css(cache-safe). Build the bundle withtopcoat asset bundle(ortopcoat dev); output is<target>/<profile>/assets. - Load before
.build():module_router!().discover().assets(AssetBundle::load().unwrap()).build(); useAssetBundle::load_dir("dist/assets")for a custom directory. Rendering a missing asset panics. - Tailwind (feature
tailwind):build.rscallstopcoat::tailwind::BuildConfig::new().render().unwrap(), thentailwind::stylesheet!()(REG/topcoat-0.10.0/docs/tailwind.md). Fixed-URL files: featurefs,public_dir("./public").
11. Testing without a socket
use topcoat::router::{Body, Router, request::Request, to_bytes};
#[tokio::test]
async fn info_refs_ok() {
let router: Router = my_app::router(test_state());
let req = Request::builder().method("GET")
.uri("/alice/repo/info/refs?service=git-upload-pack")
.body(Body::empty()).unwrap();
let res = router.handle(req).await; // http::Response<Body>
assert_eq!(res.status(), 200);
let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
assert!(!bytes.is_empty());
}
Router::handle(&self, Request) -> Response. Panics become 500. Needs noservefeature.handle_with(request, values)injects request-context values.to_bytesis exported fromtopcoat::router.
12. Differences from 0.9
module_param!is new in 0.10 (CHANGELOG: "Added: module_param! macro", PR #460). Grep found nomodule_paraminREG/topcoat-router-macro-0.9.0/src.- 0.10 changelog recommends the module router as the default example.
- Same names exist in 0.9 (existence checked only):
serve,serve_until,start,Router::handle,app_context,request_context,cookies,signed_cookies,cookie_store. - Not compared:
Cookiestrait,Compression,OriginPolicy,TrustedProxies,session. Use 0.10 source. Other 0.10 changelog items (CLI, runtime) are not relevant here.
13. Forge notes
- Smart HTTP routes:
GET /{o}/{r}/info/refs?service=..(query struct),POST /{o}/{r}/git-upload-pack,POST /{o}/{r}/git-receive-pack. Use explicit#[route]paths withpath_param!. - Pack bodies: take
Body(streaming). The 2 MiB default would reject pushes otherwise. UseBodyLimit::disable()or a largemaxon receive-pack. - Compression off on pack routes (7e). Serve pack output via
Bodyfrom child stdout (4b). No request timeout by default (7d). - Repos root, config, and DB pool go in one
AppState(section 8). - Web sessions:
private_cookieswith a persistedKey,override_prefix_host(),HttpOnly,SameSite=Lax. - Escape all user text (commit messages, paths). Never
Unescapedfor user data. - Internal API on a Unix socket via
serve_until(section 9).client_ipis None there.
14. UNVERIFIED
- Facade path for
Unescaped<T>. - Per-route compression disable (only
Compression::off()on the builder is confirmed). BodyLimit::ataccepting{param}segments.ForwardedHeader/TrustedProxiesvariant names.- Multipart field API.
sessionAPI. - Whether the router kills child processes on client disconnect (not tested).
- Whether a 401 from
unauthorized()sendsWWW-Authenticate. - Exact Origin/Fetch-Metadata headers sent by git clients.
HeaderMapinsideview!for#[page]headers (documented, not run).- Sections 8, 9, 11 snippets: derived from signatures, not compiled or run.