Firebase Hosting
Fireemu serves one Firebase Hosting site from the hosting entry of firebase.json: static files, redirects, rewrites, custom headers, clean URLs, trailing-slash handling and localized content. A rewrite can send a request to a function of a Functions codebase in the same run. Hosting is verified against production for its recorded cases: the strict profile, the default, answers as Firebase Hosting answered 201 requests recorded twice on preview channels, apart from the known differences listed below, and the emulator profile is compared offline with firebase-tools 15.28.2 on a finite set of cases and makes no production claim (see Profiles and Verification status).
Hosting is available since v0.45.0. Behaviour that changed in a later release is listed under Changes in v0.47.0 and Changes in v0.49.0 below.
Start Hosting
Section titled “Start Hosting”Hosting starts when firebase.json has a hosting object with a public directory. No other setting is needed:
{ "hosting": { "public": "public" }}npx fireemu up- The default port is 5000. If the port is busy and you did not configure one, Fireemu searches upward and never takes 5001, as
firebase-toolsdoes. A port you set is never moved. Set it withemulators.hosting.portinfirebase.json,daemon.hostingPortin a Fireemu configuration file, or--hosting-port. - The Emulator Hub lists the bound address under
hosting(GET /emulatorson the Hub). --onlyselects services as for the other products:--only hostingserves only Hosting and refuses to start when there is nohosting.public. A run that does not name Hosting keeps starting when itshostingentry is one Fireemu cannot serve (for example a list of sites), notes the entry and does not start Hosting; addhostingto--onlyto make that an error.hosting.publicis resolved from the directory offirebase.json. A missing directory is not an error, as infirebase-tools: the site starts and answers 404.
What firebase.json can set
Section titled “What firebase.json can set”These keys of the hosting object are honored:
| Key | Behaviour |
|---|---|
public |
The directory served. Required. Files are opened through a retained handle to that directory; a symbolic link that leaves it, a nonregular file and a request path with a backslash, a colon or .. are refused (answered as not found). |
ignore |
Glob patterns of files that are absent on every route, matched the way firebase-tools matches them when it chooses the files to deploy. **/firebase-debug.log, **/firebase-debug.*.log and .firebase/* are always ignored, because firebase deploy never uploads them. |
redirects |
Checked in order before any file is served. A rule has source, glob or regex, a destination and a type (default 301). :name captures in a source fill the same names in the destination. The query string is kept. |
rewrites |
Used when no file matches. source, glob or regex choose the request; the target is a destination file, a function, or a run service (see Rewrites to functions). The first matching rule wins. |
headers |
Rules with source, glob or regex and a headers list of key and value. They are added to responses for files and rewrites. |
cleanUrls |
/about.html redirects (301) to /about, and /about serves about.html. |
trailingSlash |
true adds a trailing slash to directory URLs, false removes it (301), and unset leaves a directory URL without a slash redirecting to the one with it unless cleanUrls is on. |
i18n |
{ "root": "/locales" } (any spelling of the slashes: locales, /locales/) serves locales/<language>_<country>/…, locales/ALL_<country>/…, locales/<language>_ALL/… and locales/<language>/… before the plain path, chosen from Accept-Language and the X-Country-Code header, or from the firebase-language-override and firebase-country-override cookies. |
Behaviour of a request, in order:
- Requests for
/__/firebase/init.jsonand/__/firebase/init.jsare answered first:init.jsonholds the project ID and a non-secret local API key (fireemu-local-only), andinit.js?useEmulator=truealso points the Firebase SDKs at the emulators of this run (Authentication, Firestore, Functions, Storage and the Realtime Database, when they run). Underemulatorthey take precedence over a catch-all redirect; understrictthey are answered after the redirects, as production answered (see Profiles). - A matching entry of
redirectsanswers with its status and aLocation. - A file is served when one matches, including
index.htmlfor a directory;HEADis answered with the headers only. Responses carryETag,Last-ModifiedandContent-Length, and the body is streamed from the open file. TheContent-Typecomes from the extension (compared without regard to case) in the table ofmime-types2.1.35 overmime-db1.52.0, which is what thefirebase-toolsemulator uses, including its charsets (application/javascript; charset=utf-8,text/css; charset=utf-8,image/svg+xmlwithout one). A compressible type also getsVary: Accept-Encoding, as the emulator’s compression does. - A matching entry of
rewritesanswers: adestinationfile is served with 200 (a rewrite to/404.htmlanswers 404), and afunctionorrunrule is forwarded to the local Functions runtime. - Otherwise
404.htmlfrom the public directory is served with status 404 or, without one, a built-in “Cannot GET” page.
/__/firebase/<version>/<file> (the hosted Firebase SDK scripts) is answered with 503, because Fireemu never downloads them: use a bundled SDK or a copy you serve yourself.
Rewrites to functions
Section titled “Rewrites to functions”A rewrite with a function or run target is forwarded to the Functions emulator of the same run, which must have a codebase loaded:
{ "hosting": { "public": "public", "rewrites": [ { "source": "/api/**", "function": "api" }, { "source": "/images/**", "function": { "functionId": "resize", "region": "europe-west1" } }, { "source": "/service/**", "run": { "serviceId": "api", "region": "us-central1" } }, { "source": "**", "destination": "/index.html" } ] }}- The function ID is the string after
function, itsfunctionId, or theserviceIdofrun. The region is the target’sregion, else the rule’s, elseus-central1. - A
runrewrite goes to the local function with the same ID as the Cloud Run service; Fireemu never looks up a Cloud Run service. - The request keeps its method, path, query and headers; the body is limited to 32 MiB. Fireemu adds
x-original-urlandx-forwarded-host. A response withoutCache-Controlgetsprivate, andVaryis set toAccept-Encoding, Authorization, Cookie. - Without a loaded Functions codebase the rewrite answers 503, and an upstream that cannot be reached answers 502. There is no fallback to a deployed function.
Profiles
Section titled “Profiles”Hosting answers differently under the two compatibility profiles. The emulator profile answers as the firebase-tools 15.28.2 emulator does. The strict profile, the default, answers as Firebase Hosting answered the 201 requests of the production corpus (two recordings on disposable preview channels, listed in Verification status), except for two development defaults that productionExact turns off: hosting.cacheControl and hosting.clientCountry (see Development defaults). The table describes strict with productionExact; the rows that the two defaults change say so.
| Area | strict |
emulator |
|---|---|---|
| Response headers | With productionExact, files carry Cache-Control: max-age=3600 (unless a headers rule sets one); by default they carry none (hosting.cacheControl). Files carry Accept-Ranges: bytes, Vary: x-fh-requested-host, accept-encoding and a quoted SHA-256 ETag. The Vary of an i18n site’s files begins with accept-language, x-country-code. Redirects carry the headers your rules set. |
ETag and Last-Modified only, and Vary: Accept-Encoding for compressible types. |
Content-Type |
JSON types without a charset (application/json, application/manifest+json), .js and .mjs as text/javascript; charset=utf-8, .ico as image/x-icon, an unknown extension or none as text/plain; charset=utf-8. |
The mime-types table of the emulator; an unknown extension is application/octet-stream. |
| Conditional and range requests | If-None-Match and If-Modified-Since answer 304 (with the Cache-Control of the file only); a single Range answers 206 with Content-Range. OPTIONS on a file answers an empty 200 with Allow: GET, HEAD, OPTIONS. |
The whole file. |
/404.html |
A request for /404.html serves the file with 200; any other missing path serves it with 404. |
Both answer 404. |
| Redirects | The body is text/plain. Each captured path part is percent-decoded before it is put in the Location (%2f becomes /, then runs of slashes fold), and the query string is appended as sent. A parameter name is letters and _: :id2 is the parameter id followed by a 2. |
text/html body, captured parts stay encoded (%2f becomes %252f), the query is re-encoded, and :id2 is a parameter name. |
| Reserved paths | /__/auth/handler answers 200 outside the redirects; /__/firebase/init.json and init.js are subject to them, and are cacheable files (ETag, Last-Modified, the headers your rules set). |
init.json and init.js come before the redirects; /__/auth/handler is not served. |
appAssociation: "AUTO" |
/.well-known/assetlinks.json and /.well-known/apple-app-site-association are generated ([] and an empty applinks document). |
Not served. |
i18n |
With productionExact the country comes from the request’s origin, as in production, so the X-Country-Code header and the firebase-country-override cookie are ignored; by default they are honoured, as in the emulator profile (hosting.clientCountry); Accept-Language is read with its quality values (de;q=0.9, fr;q=0.8 finds fr when there is no de); the firebase-language-override cookie is honored. |
The country header and cookie are honored; quality values are not read. |
Globs of headers, rewrites and redirects |
A dot file matches * and **, and the empty part after a trailing slash matches * and the patterns that can match nothing. |
Dot files and the empty part do not match, as in the pinned superstatic. |
A request that climbs out of the site (/../x, /%2e%2e/x) |
302 to the normalized path on this origin (Cache-Control: private, empty body); production answers the same 302 to its own content origin. Nothing outside public is read. |
404 |
| Malformed percent escape | 400 with the plain text 400 Bad Request, sent chunked. |
400 with Malformed URL. |
Development defaults
Section titled “Development defaults”Two behaviours of Hosting production make local development worse, so the default is the official emulator’s answer and productionExact restores production (the same rule as the other development defaults):
hosting.cacheControl:"none"by default, so a browser shows an edited file at once;"production"addsCache-Control: max-age=3600to a file whoseheadersrules set none. A rule that setsCache-Controlwins in every mode.hosting.clientCountry:"honoured"by default, so country content can be tested with theX-Country-Codeheader or thefirebase-country-overridecookie;"ignored"reads neither, because production decides the country at its edge.Accept-Languagequality values and thefirebase-language-overridecookie are read in both modes.
Both keys apply to the strict profile only (the emulator profile always answers as the official emulator), and an explicit key wins over productionExact.
Known differences and unrecorded answers
Section titled “Known differences and unrecorded answers”Known differences that strict does not reproduce are listed in the closure record: gzip and Brotli content encoding, the body of production’s own 404 page (a longer HTML page), the hosted SDK files under /__/firebase/<version>/, the init.json keys beyond apiKey and projectId, the length of the hosted /__/auth/handler page (the answer is a stand-in page), the Strict-Transport-Security header of https and the preview channel’s X-Robots-Tag, and the host of the 302 production answers to a request that climbs out of the site. In the comparison with the recordings, 21 of the 201 rows carry a known difference.
Some strict answers come from a reading of the recording, not from an observation: a redirect parameter name is read as letters and _, so :id2 is the parameter id followed by a 2 (the recording shows only that one source with such names did not match; other readings fit it, and this applies to every redirect source with a digit in a name); an unsatisfiable Range answers 416 with Content-Range: bytes */<size>; a request with several ranges is answered whole; HEAD with a Range answers 206 with the length of the part and no body; /a/../b answers a 302 to /b, /a/./b to /a/b and /a/b/.. to /a/ (a path that ends in a dot segment ends in a slash, as RFC 3986 says); a dot-segment path with a raw backslash, control character or space is not redirected and answers 404, because a browser reads \ as / and Location: /\evil.example would leave the origin. Production was not observed on these.
Not supported
Section titled “Not supported”- More than one site: a
hostinglist,siteandtarget. A list is refused withhosting must be one site object; multiple sites are not implementedwhen Hosting is named in--only. hosting.source(web frameworks) and any site withoutpublic.- App Hosting.
--only apphostingis refused as deferred; see Limitations. - Keys other than those in the table above are not acted on.
appAssociation: "AUTO"generates the two association documents understrictonly; a file you place inpublicis served like any other. - The hosted SDK scripts under
/__/firebase/<version>/, deployed Cloud Run services and deployed functions (no live fallback). - A file with no extension, or an extension that
mime-typesdoes not know, is served asapplication/octet-stream. Thefirebase-toolsemulator sends the literal textfalseas itsContent-Typethere, and production’s behaviour was not recorded. TheContent-Typeof a custom header rule replaces the one Fireemu chooses.
Changes in v0.47.0
Section titled “Changes in v0.47.0”These changes are in v0.47.0 and later; v0.45.0 and v0.46.0 behaved differently (see the changelog):
- An
i18n.rootwritten with a leading or trailing slash (/locales, the form Firebase’s documentation shows) finds the same files aslocales, and an empty root is no root, as insuperstatic. Before, only a root without a leading slash worked. - Content types come from the
mime-types/mime-dbtable of thefirebase-toolsemulator (generated from it), with itsVary: Accept-Encodingrule. Before, a short built-in table served every other extension, such as.woff2,.gifor.ico, asapplication/octet-streamand.jsastext/javascript. ignorepatterns ignore case the wayfirebase-toolsdoes on macOS and Windows (a JavaScript regular expression with theiflag, as theglobpackage compiles them); on Linux they stay case-sensitive.headers,rewritesand globredirectssources treat a run of slashes as one, and a pattern also matches a path with one trailing slash, as the pinnedsuperstaticdoes. A percent-encoded alias such as/%64ata.jsonstill does not get a header rule, as in the pinned emulator.- A rule whose
sourceis empty matches nothing and falls through to itsregex, assuperstaticdoes.
Changes in v0.49.0
Section titled “Changes in v0.49.0”These changes are in v0.49.0 and later; earlier releases answered the same under strict and emulator and had no hosting.cacheControl or hosting.clientCountry key (see the changelog):
- The
strictprofile, the default, answers as Firebase Hosting answered the production corpus; see Profiles for each answer. Theemulatorprofile is unchanged. hosting.cacheControlandhosting.clientCountryare new development defaults;productionExactturns both to production’s behaviour.- Under
strict,/__/firebase/init.jsonandinit.jsare answered after the redirects, andappAssociation: "AUTO"generates the two association documents.
Verification status
Section titled “Verification status”The HOSTING area of the compatibility tables is Verified for the recorded cases: production was observed twice with the same 201 requests (static files, redirects, rewrites, glob matching, headers and caching, clean URLs, trailing slashes, localization, reserved paths, content types, compression, app association), on preview channels of a disposable site, never on a live channel, and the closure review of 2026-11-11 accepted the evidence. The two recordings agree with each other on every request except a production-internal redirect host, and the strict profile under productionExact agrees with both on every request except the known differences above; of the 201 rows of each comparison, 21 carry a known difference and are counted separately from matches. The claim covers the recorded requests and the rules read from them (the unrecorded and inferred answers are listed above); emulator makes no production claim, and function and Cloud Run rewrites, ignore and the local safety properties are not part of it. Gzip is the next change (owner-approved); Brotli stays a known difference. The offline conformance suite still compares Fireemu with firebase-tools 15.28.2 on a finite corpus, including over 3,000 generated rows of the real file listing used for ignore. Treat a Hosting test that passes here as a check of your firebase.json and routing: preview channel answers are not the live channel’s.