/op/switch_lang/<code> — the dedicated language-switch endpoint. Lives in src/routes/switch_lang.rs. Unlike the verb-mode routes, this is a real registered endpoint (not a dispatch branch), and it has its own URL shape.
endpoint! {
APP.url("/op/switch_lang/<lang>"),
pub switch_lang <HTTP> { /* ... */ }
}
GET only — any other method returns 405 Method not allowed. There is no auth gate; switching language is a public action available to anonymous visitors.
<code> path parameter — the target language. Run through normalize_lang, which lowercases, trims, and confirms membership in content::support_langs(). Unrecognized codes fall back to sfx::op::default_lang().from query parameter (preferred) — the source URL the visitor was viewing.Referer header (fallback) — used when from is absent or empty. Both referer and referrer spellings are accepted.The source is resolved by switch_source(req):
if let Some(from) = req.query("from") { /* if non-empty, use it */ }
if let Some(referer) = req.header_str("referer").or_else(|| req.header_str("referrer")) {
/* if non-empty, use it */
}
"/".to_string()
from wins because it is set explicitly by the language-switcher widget; Referer is the graceful fallback for bookmarklets, direct hits, and anything else that doesn’t pass from.
Extract path. normalize_source_url accepts either an absolute URL or a path. If it starts with /, take it verbatim. If it has a ://, extract the path component after the host. Anything else collapses to /.
Strip the current lang prefix. strip_supported_lang_prefix walks content::support_langs() and removes a leading /<lang>/ (or bare /<lang>) if present. This is what prevents prefix stacking — switching from /zh/policies/ to JA must produce /ja/policies/, not /ja/zh/policies/.
Open-redirect hardening. After path extraction:
if path.is_empty() || path.starts_with("//") {
"/".to_string()
} else {
path
}
Both ?from=//evil.com/... and Referer: http://host//evil.com/... previously slipped through: the host-stripping step left //evil.com/... as the “path,” and emitting Location: //evil.com/... would let a browser navigate off-origin (a network-path reference, classic open-redirect vector). The final-step rejection catches both routes regardless of which input fed them.
Apply target prefix. Empty ("") for the default language, /<code> otherwise:
let target_path = if target_lang == sfx::op::default_lang() {
stripped
} else {
format!("/{}{}", target_lang, stripped)
};
format!("{}{}", target_path, query)
Query strings are preserved verbatim — switching language on /foo/?view=download lands the visitor on /zh/foo/?view=download.
The response sets a lang cookie:
.add_cookie(
"lang",
Cookie::new(target_lang)
.path("/")
.http_only(true)
)
.add_header("Cache-Control", "no-store")
HttpOnly because no client JS needs to read it. Cache-Control: no-store because the response body is a 302 whose target depends on Set-Cookie — caching it (intermediate proxy or browser back/forward cache) would mean a subsequent visit replays the redirect without storing the cookie, defeating the whole switch.
302 Found to the rewritten URL. No body of consequence; the browser follows the Location header and the next request carries both the new URL and the new cookie.
Visitors on bare URLs (the default-lang URL scheme) have no /<code> segment for Route: view (dispatch) to read, so language resolution falls back to sfx::op::lang_or_none(req), which consults the cookie. Without it, every bare-URL hit serves the default language — pressing the JA switcher on /foo/ would land you on /foo/ again with the default content, because there’s no signal anywhere telling the renderer “this user wants Japanese.”
The cookie also makes language sticky across sessions: a visitor who picks JA once gets JA content on every later bare-URL visit until they switch again. See Language system for the full resolution order (prefix > cookie > default).
Without strip_supported_lang_prefix, switching from a non-default language to another non-default language would stack prefixes:
/zh/policies/ clicks “JA”/ja + /zh/policies/ = /ja/zh/policies/ (a 404, because no such route exists)/ja + /policies/ = /ja/policies/The strip step normalizes the source path to its bare form before applying the new prefix, so the switch is symmetric regardless of where the visitor started.
For the full language resolution stack (prefix > cookie > default), the build-generated /<lang>/<**path> endpoints, and how lang_for(req) decides which .md slot to render: Language system.