DEV Community

octave Nkurunziza
octave Nkurunziza

Posted on Edited on Originally published at blog.hubfly.space

Invalid Host header after deploying? Why it happens and the fix

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 curl check 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
Enter fullscreen mode Exit fullscreen mode

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: …

Illustration of the Vite, webpack-dev-server, Django and MCP SDK responses to an unknown 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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 },
})
Enter fullscreen mode Exit fullscreen mode

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'],
  },
};
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"},
    },
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

  1. Open your project in the Hubfly space dashboard and create or open your container.
  2. 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.
  3. Copy that address. Under Configuration → Environment Variables, add ALLOWED_HOSTS (and for Django, CSRF_TRUSTED_ORIGINS with https:// in front).
  4. Redeploy or restart the container so the app reads the new values.

Hubfly space Environment Variables step with ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS set to the app's public address.

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/
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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)