forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
26.0 KiB

Topcoat 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

// src/app.rs  (module maps to "/")
use topcoat::router::{Router, module_router};
pub fn router() -> Router { module_router!().discover().build() }
  • module_router!() returns a RouterBuilder. .discover() (trait topcoat::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) generates Name (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); in src/app/posts/post_id.rs serves /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") (feature fs, 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, or Response<()> sets the status; middle items are IntoResponseParts (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))))
}
  • bytes is re-exported as topcoat::router::request::Bytes.
  • Caveat: child drops when this function returns. With kill_on_drop(true) the process would die immediately, so either omit it or move the Child into the stream (a struct owning child and the stream). Without kill_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)) then tokio::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, let in 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; Slot for layouts; cx: &Cx optional, any order.
  • Escaping: yes, auto-escaped. Text and (expr) interpolations escape <, &, etc., and attribute values are escaped too (tests in REG/topcoat-view-0.10.0/src/buffer/handle.rs, .../src/html/attribute.rs). Opt-out is push_*_unescaped and an Unescaped<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) plus class!.
  • A #[page] sets status or headers with a StatusCode or HeaderMap element inside view! (per REG/topcoat-0.10.0/docs/router.md; not run). For git endpoints use #[route] returning Response or 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/remove accept Into<Cookie>, so jar.add(("k", "v")) works.
  • Attributes: Path, Domain, HttpOnly, Secure, SameSite, MaxAge, set via builder or cookie!("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("/"). The override_* form forces a value. Wrap this in one helper fn cookies(cx) -> impl Cookies.
  • __Host-: works. jar.override_prefix_host() stores __Host-name, forces Secure, Path=/, no Domain; reads strip the prefix. override_prefix_secure() gives __Secure-. Browsers reject Secure cookies over plain http:// except on localhost (UNVERIFIED for your deployment).
  • Signed/private: signed_cookies(cx), private_cookies(cx) need a Key in 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 until commit().
  • 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 as Err from next.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 by Bytes, String, Json, Form. Over it: 413. Body is 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/src and REG/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 with TimeoutLayer (7b).

7e. Compression (on by default; matters for git)

  • Negotiated from Accept-Encoding after 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-*-result is not excluded, so pack responses would be compressed.
  • Fix (applied): Router::builder().compression(Compression::off()) on the public router, plus crates/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 Arc inside for mutable resources (pools, locks).
  • Per-request values: request_context::<T>(cx) / try_request_context, added by a layer with cx.with(value).
  • Background work: let cx = cx.clone() and move it into tokio::spawn. A cloned Cx still 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(())
}
  • serve and start each install their own signal handlers, so use serve_until for several listeners. Unix listener: remote_addr and client_ip return None. 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 with topcoat asset bundle (or topcoat dev); output is <target>/<profile>/assets.
  • Load before .build(): module_router!().discover().assets(AssetBundle::load().unwrap()).build(); use AssetBundle::load_dir("dist/assets") for a custom directory. Rendering a missing asset panics.
  • Tailwind (feature tailwind): build.rs calls topcoat::tailwind::BuildConfig::new().render().unwrap(), then tailwind::stylesheet!() (REG/topcoat-0.10.0/docs/tailwind.md). Fixed-URL files: feature fs, 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 no serve feature.
  • handle_with(request, values) injects request-context values. to_bytes is exported from topcoat::router.

12. Differences from 0.9

  • module_param! is new in 0.10 (CHANGELOG: "Added: module_param! macro", PR #460). Grep found no module_param in REG/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: Cookies trait, Compression, OriginPolicy, TrustedProxies, session. Use 0.10 source. Other 0.10 changelog items (CLI, runtime) are not relevant here.

13. Forge notes

  1. 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 with path_param!.
  2. Pack bodies: take Body (streaming). The 2 MiB default would reject pushes otherwise. Use BodyLimit::disable() or a large max on receive-pack.
  3. Compression off on pack routes (7e). Serve pack output via Body from child stdout (4b). No request timeout by default (7d).
  4. Repos root, config, and DB pool go in one AppState (section 8).
  5. Web sessions: private_cookies with a persisted Key, override_prefix_host(), HttpOnly, SameSite=Lax.
  6. Escape all user text (commit messages, paths). Never Unescaped for user data.
  7. Internal API on a Unix socket via serve_until (section 9). client_ip is None there.

14. UNVERIFIED

  • Facade path for Unescaped<T>.
  • Per-route compression disable (only Compression::off() on the builder is confirmed).
  • BodyLimit::at accepting {param} segments.
  • ForwardedHeader / TrustedProxies variant names.
  • Multipart field API. session API.
  • Whether the router kills child processes on client disconnect (not tested).
  • Whether a 401 from unauthorized() sends WWW-Authenticate.
  • Exact Origin/Fetch-Metadata headers sent by git clients.
  • HeaderMap inside view! for #[page] headers (documented, not run).
  • Sections 8, 9, 11 snippets: derived from signatures, not compiled or run.