Nicholas Pini
09/30/2025, 11:18 AMMarvin
09/30/2025, 11:18 AMMarvin
09/30/2025, 11:22 AMAUTH=$(printf '%s' 'admin:pass' | base64)
curl -X POST http://<host>:4200/api/flows \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d '{"name": "my-flow"}'
- Python requests
import requests
r = <http://requests.post|requests.post>(
"http://<host>:4200/api/flows",
json={"name": "my-flow"},
auth=("admin", "pass") # sets Basic Authorization automatically
)
print(r.status_code, r.text)
Common pitfalls that cause 401 even when “auth is set”:
- Setting the auth string on the server but not sending it with the client request. Environment variables like PREFECT_API_AUTH_STRING help Prefect’s Python client, but raw HTTP tools (curl/Postman) must send the header explicitly.
- Using a Bearer token (PREFECT_API_KEY) instead of Basic auth. Bearer is for Prefect Cloud; self-hosted Server expects Basic.
- Reverse proxies/load balancers stripping the Authorization header. Ensure your proxy forwards it.
- Small formatting issues: wrong base64 value (must be base64 of exactly user:pass), trailing spaces/newlines, wrong username/password.
Docs and references:
- Prefect 3 auth for self-hosted server: Security settings (Server auth)
- Your server’s OpenAPI docs (helpful to test endpoints): http//<host>4200/docs
If you share the exact request you’re sending (headers minus secrets) and whether you’re behind a proxy, I can pinpoint the issue quickly.Nicholas Pini
09/30/2025, 11:23 AMMarvin
09/30/2025, 11:23 AMMarvin
09/30/2025, 11:26 AMauth_string
(username:password), not a Bearer API key.
- If PREFECT_API_KEY
is set anywhere (env/profile), it takes precedence and the client will send Bearer auth — which Server will reject with 401.
How to fix and reliably create a flow
Option A — pass auth_string explicitly
import asyncio
from prefect.client.orchestration import PrefectClient
from prefect.client.schemas.actions import FlowCreate
async def main():
client = PrefectClient(
api="http://<host>:4200/api",
auth_string="admin:pass", # match your server.api.auth_string
)
await client.create_flow(FlowCreate(name="my-flow"))
asyncio.run(main())
Option B — use environment variables (no code changes)
export PREFECT_API_URL=http://<host>:4200/api
export PREFECT_API_AUTH_STRING=admin:pass
# Make sure PREFECT_API_KEY is NOT set anywhere
prefect config view --show-sources
Then:
import asyncio
from prefect.client.orchestration import get_client
from prefect.client.schemas.actions import FlowCreate
async def main():
async with get_client() as client:
await client.create_flow(FlowCreate(name="my-flow"))
asyncio.run(main())
Common causes of 401 with PrefectClient.create_flow
- PREFECT_API_KEY is set in your environment/profile, so the client uses Bearer auth instead of Basic. Remove it or switch to a clean profile.
- Check with:
prefect config view --show-sources
- If you see PREFECT_API_KEY
, remove it from your profiles.toml or unset it in the environment.
- The reverse proxy is stripping the Authorization
header. If you’re routing through a proxy, make sure it forwards Authorization
.
- Small mismatches: wrong auth_string
value, trailing spaces/newlines, or using the wrong API base (it should be http://<host>:4200/api
).
Docs
- Server auth for self-hosted: Security settings (Server auth)
- Python client reference: PrefectClient API
If you can share how you’re constructing the client (code snippet) and a quick prefect config view --show-sources
(redact secrets), I can pinpoint exactly what’s causing the 401.Nicholas Pini
09/30/2025, 11:38 AMMarvin
09/30/2025, 11:38 AMMarvin
09/30/2025, 11:39 AMPREFECT_API_URL
is not set, and Cloud will return 401 without a Bearer token. Locally you likely have a profile or env var set for `PREFECT_API_URL`; in CI you only set the auth string.
What to do in your CI job
- Set BOTH of these environment variables:
- PREFECT_API_URL=http://<host>:4200/api
- PREFECT_API_AUTH_STRING=admin:pass
- Ensure PREFECT_API_KEY
is NOT set anywhere (env or profile).
- Avoid wrapping the auth string in quotes in the secret value (no leading/trailing quotes or whitespace).
- If you’re behind a proxy, make sure it does not strip the Authorization
header.
For extra safety in code, you can pass these explicitly:
import os, asyncio
from prefect.client.orchestration import PrefectClient
from prefect.client.schemas.actions import FlowCreate
async def main():
client = PrefectClient(
api=os.environ["PREFECT_API_URL"], # e.g. <http://host:4200/api>
auth_string=os.environ["PREFECT_API_AUTH_STRING"], # e.g. admin:pass
server_type="server", # force server mode detection
)
await client.create_flow(FlowCreate(name="my-flow"))
asyncio.run(main())
Quick CI diagnostics
- Print the active config at runtime (redact secrets) to confirm the URL and auth source:
prefect config view --show-sources
- From the CI runner, try a basic call to verify the target and auth:
python - <<'PY'
import os, requests, base64
url = os.environ.get("PREFECT_API_URL", "") + "/flows"
auth = os.environ["PREFECT_API_AUTH_STRING"]
b = base64.b64encode(auth.encode()).decode()
r = requests.get(url, headers={"Authorization": f"Basic {b}"})
print(r.status_code, r.url, r.text[:200])
PY
If you’ve set PREFECT_API_URL
and still get 401 in CI:
- Check for trailing newlines/whitespace in PREFECT_API_AUTH_STRING
(many CI systems can add them).
- Confirm the CI network/proxy isn’t stripping Authorization
.
- Verify Prefect versions are the same locally and in CI (3.4.9 SDK against 3.4.9 server is good).
Docs
- Server auth (self-hosted): Security settings (Server auth)
- PrefectClient reference: PrefectClient API
If you share your CI step env setup (redacting secrets) and the output of prefect config view --show-sources
from CI, I can help pinpoint the exact issue.