TL;DR
- What you get: the exact error each framework prints when it rejects your public address, the one-line fix for each, and a 10-second
curlcheck to catch it before you deploy.- Who it's for: anyone who just put a Vite, React, Vue, Django or MCP server app online and got a blank 403, 400 or 421 instead of their page.
- Time needed: 10–15 minutes.
Checked in October 2026 with Vite 8, webpack-dev-server 6, Django 6.1 and the Python MCP SDK 2.x.
Your app worked on localhost. You deployed it, opened the public URL, and got a one-line page: Invalid Host header. Or Blocked request. This host is not allowed. Or a bare Bad Request (400) with nothing in the logs.
Nothing is broken on the server. Your app is refusing visitors on purpose, because it doesn't recognise the address they typed. In this guide you'll make your app accept its real address, and only that address, so your site loads and stays protected. It takes one setting per framework.
Try it on Hubfly space: you get a free HTTPS address for your app on the free tier (360 hours a month, no card).
Why your app rejects its own address
Every browser request carries a Host header: the name the visitor typed, like shop-web.eu1.hubfly.app. On your laptop that name is always localhost or 127.0.0.1. Once you're online, the platform's edge proxy handles HTTPS and passes the public name through to your container unchanged.
Browser ──Host: shop-web.eu1.hubfly.app──▶ HTTPS proxy ──same Host──▶ your container
│
"Is this name on my list?" ┘
no → 403 / 400 / 421
Many dev servers and frameworks keep a list of names they'll answer to. By default that list is only localhost. This protects you from DNS rebinding, an attack where a malicious website tricks a browser into talking to a server it shouldn't reach. So the check is good. It just doesn't know your new name yet.
Each tool fails differently, which is why the error is so confusing:
| What you run | What visitors see | Status | Where it's logged |
|---|---|---|---|
Vite dev server (vite) |
Blocked request. This host ("…") is not allowed. |
403 | Not in server output |
vite preview |
Same message, pointing at preview.allowedHosts
|
403 | Not in server output |
| webpack-dev-server (Vue CLI, older React and Angular setups) | Invalid Host header |
403 | Not in server output |
Django with DEBUG = False
|
Bad Request (400) |
400 | Nowhere by default |
Python MCP SDK (streamable_http_app()) |
Invalid Host header |
421 | WARNING Invalid Host header: … |
The same request, four tools, four different answers.
Step 1: Reproduce it on your laptop in 10 seconds
You don't need to deploy to see the error. Start your app locally, then send it the public name with curl:
# Replace the port and the name with yours
curl -i -H "Host: your-app.eu1.hubfly.app" http://127.0.0.1:5173/
If you get a 403, 400 or 421, you've found the problem without waiting for a deploy. Keep this command handy: you'll run it again after each fix.
Why this works: the check only looks at the Host header, not at how the request reached your app. Faking the header locally triggers exactly what your visitors see.
Step 2: Stop running a dev server in production (frontends)
If the error comes from Vite or webpack-dev-server, the real fix is usually not a setting. It's that you deployed the development server. Dev servers rebuild on every change, use more memory, and carry these local-only safety checks.
Build your site once and serve the static files instead. For a Vite app (React, Vue, Svelte, Solid), add a static file server:
npm install sirv-cli
Then set your scripts in package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "sirv dist --host 0.0.0.0 --port ${PORT:-8080} --single"
}
}
Your platform runs npm run build once, then npm start. A static file server has no host check, so the error disappears. --single sends unknown paths like /orders/42 to index.html, which single-page apps need for client-side routing. --host 0.0.0.0 makes the server reachable from outside the container. (Binding to 127.0.0.1 instead gives you a 502; see our guide to the 502 Bad Gateway fix.)
On Hubfly space, set the container port to the one in your start script (8080 here).
Step 3: When you do need the check, allow your real name
Sometimes you really do want the server running online: a shared preview, a Django app, an MCP server. Then add your public name to the allowlist. List the exact names. Never switch the check off.
Vite (dev and preview)
// vite.config.js
import { defineConfig } from 'vite'
const hosts = (process.env.ALLOWED_HOSTS || '').split(',').filter(Boolean)
export default defineConfig({
server: { host: '0.0.0.0', allowedHosts: hosts },
})
Set ALLOWED_HOSTS=your-app.eu1.hubfly.app in the environment. A leading dot allows a domain and all its subdomains: .your-domain.com covers staging.your-domain.com too. vite preview uses server.allowedHosts unless you set preview.allowedHosts separately, so one list covers both.
Don't use allowedHosts: true. It accepts any name, which turns the protection off.
webpack-dev-server (Vue CLI and custom webpack setups)
// webpack.config.js
module.exports = {
// ...
devServer: {
host: '0.0.0.0',
allowedHosts: ['your-app.eu1.hubfly.app', '.your-domain.com'],
},
};
The same leading-dot rule applies. Old answers suggest disableHostCheck: true or allowedHosts: 'all'. The first was removed in webpack-dev-server 4 (current versions refuse to start with it), and the second turns the check off.
Django: two settings, not one
Django has two checks, and the second only shows up after you fix the first.
1. ALLOWED_HOSTS blocks unknown names with Bad Request (400). With DEBUG = False, Django logs this through its mail_admins handler only. Gunicorn's output stays empty, which is why this one feels like a ghost. Read the list from the environment:
# settings.py
import os
ALLOWED_HOSTS = [h for h in os.environ.get("ALLOWED_HOSTS", "").split(",") if h]
CSRF_TRUSTED_ORIGINS = [o for o in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",") if o]
2. CSRF_TRUSTED_ORIGINS matters as soon as someone submits a form, including the admin login. The platform's proxy handles HTTPS and talks to your app over plain HTTP inside the network. So the browser says the request came from https://your-app…, Django thinks it's serving http://your-app…, and every POST fails with 403 CSRF verification failed. With DEBUG = True the page shows the real reason: Origin checking failed - https://your-app.eu1.hubfly.app does not match any trusted origins.
Set both values in your container's environment:
ALLOWED_HOSTS=your-app.eu1.hubfly.app
CSRF_TRUSTED_ORIGINS=https://your-app.eu1.hubfly.app
Note the difference: ALLOWED_HOSTS takes bare names, while CSRF_TRUSTED_ORIGINS needs the scheme (https://).
To make the next host error visible in your logs, send Django's host warnings to the console:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {
"django.security.DisallowedHost": {"handlers": ["console"], "level": "ERROR"},
},
}
Now an unknown name prints Invalid HTTP_HOST header: 'your-app.eu1.hubfly.app'. You may need to add 'your-app.eu1.hubfly.app' to ALLOWED_HOSTS. in your container logs.
Python MCP server: the 421 error
The official Python MCP SDK switches on host checking when the app is built for 127.0.0.1, which is the default for streamable_http_app(). Running it with uvicorn ... host="0.0.0.0" doesn't change that, so your public URL gets 421 Invalid Host header while curl localhost works fine.
Pass your public name explicitly:
import os
import uvicorn
from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
mcp = MCPServer("orders")
@mcp.tool()
def ping() -> str:
return "pong"
public_host = os.environ["PUBLIC_HOST"] # e.g. your-mcp.eu1.hubfly.app
app = mcp.streamable_http_app(
transport_security=TransportSecuritySettings(
allowed_hosts=[public_host],
allowed_origins=[f"https://{public_host}"],
)
)
uvicorn.run(app, host="0.0.0.0", port=8000)
Your public name now gets a normal response, and any other name still gets 421. That includes 127.0.0.1: if a local health check calls the server, add 127.0.0.1:8000 to allowed_hosts too. The SDK's own mcp.run(transport="streamable-http", host="0.0.0.0") skips the host check entirely; only use it if the server sits behind an access key. For the full MCP setup with HTTPS and an access key, see Deploy your own MCP server.
Step 4: Put the name in your environment on Hubfly space
Keep the hostname out of your code so the same build works for staging, production and your own domain.
- Open your project in the Hubfly space dashboard and create or open your container.
- Go to Network → Add Domain → Auto Endpoint. You get an HTTPS address like
your-app.eu1.hubfly.app, with TLS and force-HTTPS switched on. - Copy that address. Under Configuration → Environment Variables, add
ALLOWED_HOSTS(and for Django,CSRF_TRUSTED_ORIGINSwithhttps://in front). - Redeploy or restart the container so the app reads the new values.
The Environment Variables tab with both values set.
Adding your own domain later? Add it to the same variables, separated by a comma: ALLOWED_HOSTS=your-app.eu1.hubfly.app,www.your-domain.com. For a custom domain, add the DNS record the dashboard shows; Hubfly space issues the certificate and renews it 30 days before it expires.
Step 5: Check it
Open your public URL in the browser, or run:
curl -i https://your-app.eu1.hubfly.app/
Once your name is on the list, you get your page instead of a 403, 400 or 421. Then try a name you didn't allow, against your local copy:
curl -i -H "Host: not-mine.example.com" http://127.0.0.1:8000/
That one should still be refused. If both behave, the fix is done and the protection is still on.
What can go wrong
1. "I fixed ALLOWED_HOSTS, now the admin login says CSRF verification failed."
That's the second Django check. Add CSRF_TRUSTED_ORIGINS=https://your-app.eu1.hubfly.app, with the scheme. If your proxy sets X-Forwarded-Proto, SECURE_PROXY_SSL_HEADER is the other route, but only use it when the proxy always overwrites that header. Otherwise a client could fake it.
2. "I added the name, but it's still blocked."
Check what you typed against the error text: www.your-domain.com and your-domain.com are different names. In Vite and webpack a leading dot covers subdomains; without the dot it matches one exact name. Also check the app actually restarted: dev servers and Django read these settings at startup.
3. "It works on the platform URL but not my custom domain."
Each name your visitors use must be on the list. Add the custom domain (and www, if you use it) to ALLOWED_HOSTS, and to CSRF_TRUSTED_ORIGINS with https:// for Django.
4. "It works, but my bundle is huge and the page is slow."
You're probably still running the dev server. Go back to Step 2 and serve the built files.
Takeaway
"Invalid Host header" means your app's safety check doesn't know its new name. For frontends, the best fix is to stop shipping the dev server and serve your build. For everything else, put your exact public name in an environment variable and feed it to the framework's allowlist. Never turn the check off. And before your next deploy, run one curl -H "Host: …" locally: it catches the problem in 10 seconds.
Read next: 502 Bad Gateway in Docker? Bind to 0.0.0.0 · Build failed? How to read the build log · Deploy your own MCP server with HTTPS and an access key
Go further on Hubfly space
-
Put it on your own domain: add a custom domain to your container with automatic HTTPS, then add it to
ALLOWED_HOSTS. Domains docs - Add a database next to it: a managed PostgreSQL database starts at about $2 a month. Managed databases
-
Keep a private staging copy: run a second container with its own Auto Endpoint and its own
ALLOWED_HOSTS, and turn on Identity Access (API Key Only) from the project's Firewall. Open the dashboard
Deploy this
Ship your app with its own HTTPS address on the free tier: the shared tier is free within 360 hours a month, no card needed. An always-on dedicated container (0.5 vCPU, 512 MB RAM, 4 GB storage, Auto Sleep off) costs about $2.23 a month. Start on Hubfly space →


Top comments (0)