André Lynum
06/16/2026, 8:50 AMMarvin
06/16/2026, 8:51 AMMarvin
06/16/2026, 8:53 AMasyncio.run(), or pytest-asyncio
- Process/subprocess differences — CI Linux runners can behave differently than local machines, especially with multiprocessing
- Prefect API/server setup — tests may implicitly rely on an ephemeral API or local server that is not available/ready in CI
- Environment/settings differences — PREFECT_API_URL, profiles, temp settings, or secrets may differ in Actions
- Timing/resource issues — CI is slower, so race conditions and short timeouts show up there
Prefect-specific things to check
1. Are you running a task directly, or inside a flow?
- Prefect tasks are safest to test either:
- as plain Python by calling the underlying function logic, or
- inside a flow run if you want orchestration behavior
2. If async: avoid asyncio.run() inside pytest async tests
- Prefer:
@pytest.mark.asyncio
async def test_task():
result = await my_async_fn()
3. If using task runners/processes: prefer threads in tests
- Process-based execution is more likely to fail in CI than locally.
4. If the test touches the Prefect API: verify your CI env
- Check whether PREFECT_API_URL is set
- If you expect ephemeral mode, make sure the test is actually configured for it
5. Increase timeouts in CI
- A test that passes locally can fail in GitHub Actions just because the runner is slower
Typical fixes
- Use @pytest.mark.asyncio for async tests
- Don’t call asyncio.run() from within an already-async test
- Avoid process-based task runners in unit tests
- Wrap Prefect settings with temporary_settings(...) in the test
- If using a server, wait until it’s healthy before running assertions
- Make CI timeouts more generous
Very common anti-pattern
# fragile in CI
def test_task():
result = my_task.submit().result(timeout=2)
assert result == 1
Better:
from prefect import flow, task
@task
def my_task():
return 1
@flow
def test_flow():
return my_task()
def test_task():
assert test_flow() == 1
If you do want to test .submit() behavior, it helps to share:
- the test code
- the full GitHub Actions error/traceback
- whether the task is sync or async
- whether it runs inside a flow
- whether you set PREFECT_API_URL
If you paste the failing test and traceback, I can usually pinpoint the exact cause quickly.
Relevant docs:
- Prefect docs
- Deployments and execution modelAndré Lynum
06/16/2026, 9:25 AMapi_version = self.api_version()^^^^^^^^^^^^^^^^^^ /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/prefect/client/orchestration/__init__.py1476 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/prefect/client/orchestration/__init__.py1451 in api_version res = self._client.get("/admin/version") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/httpx/_client.py1053 in get return self.request( /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/httpx/_client.py825 in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/prefect/client/base.py677 in send response.raise_for_status() _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <Response [500 Internal Server Error]> def raise_for_status(self) -> Response: """ Raise an exception if the response contains an HTTPStatusError. The
PrefectHTTPStatusError contains useful additional information that
is not contained in the HTTPStatusError.
"""
try:
return super().raise_for_status()
except HTTPStatusError as exc:
raise PrefectHTTPStatusError.from_httpx_error(exc) from exc.causeE prefect.exceptions.PrefectHTTPStatusError: Server error '500 Internal Server Error' for url 'http://127.0.0.1:8949/api/admin/version' - Response: {'exception_message': 'Internal Server Error'} - For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/prefect/client/base.py164 PrefectHTTPStatusError The above exception was the direct cause of the following exception: mock_teams_webhook = TeamsWebhook(webhook_url=SecretStr('**********')) def test_alert_formats_message_correctly(mock_teams_webhook): """ Test: alert formats message correctly. Verifies that send_teams_alert formats the Teams MessageCard with all required fields and proper structure. """ mock_response = MagicMock() mock_response.status_code = 200 mock_response.raise_for_status = MagicMock() with patch("src.tasks.alerting.requests.post", return_value=mock_response) as mock_post:
result = send_teams_alert(webhook=mock_teams_webhook, title="Pipeline Failed", message="COPY INTO failed", facts=[ {"name": "Domain", "value": "demo"}, {"name": "Environment", "value": "test"}, ], status="error", flow_run_url="https://app.prefect.cloud/runs/123" ) tests/test_alerting.py38
Marvin
06/16/2026, 9:25 AMMarvin
06/16/2026, 9:26 AMsend_teams_alert(...), something is causing Prefect to create a SyncPrefectClient, and that client is trying to talk to a local API at:
<http://127.0.0.1:8949/api/admin/version>
That request returns 500, and then Prefect raises the version-mismatch/client error.
So the failure is not primarily GitHub Actions itself — it’s that in CI your test ends up hitting a Prefect API server, and that server is unhealthy or misconfigured.
Why this happens
Usually one of these is true:
1. Your task/function calls Prefect runtime/client APIs internally
- e.g. get_run_logger()
- prefect.runtime.flow_run.*
- prefect.runtime.task_run.*
- get_client()
- block loading or artifact/event APIs
2. The code is decorated with @task and you are invoking the task object in a way that triggers Prefect orchestration
- instead of testing the pure Python function body
3. A local ephemeral API is being started automatically in the test environment
- and in GitHub Actions it starts badly, incompletely, or with missing dependencies/state
Most likely root cause here
Because the URL is 127.0.0.1:8949, this looks like an ephemeral/local Prefect API rather than Prefect Cloud.
That means your test likely depends on Prefect runtime context even though it looks like a simple unit test.
What I’d check first
In send_teams_alert, look for anything like:
from prefect import task
from prefect.logging import get_run_logger
from prefect.runtime import flow_run, task_run
from prefect.client.orchestration import get_client
from prefect.blocks.core import Block
Especially get_run_logger() is a common trigger when code is called outside a proper flow/task run context.
Best fixes
Option 1: Make this a pure unit test
If send_teams_alert is supposed to just build a payload and call <http://requests.post|requests.post>, separate the business logic from Prefect:
def build_teams_payload(title, message, facts, status, flow_run_url=None):
return {
# payload here
}
@task
def send_teams_alert(webhook, title, message, facts, status, flow_run_url=None):
payload = build_teams_payload(title, message, facts, status, flow_run_url)
response = <http://requests.post|requests.post>(webhook_url, json=payload)
response.raise_for_status()
return True
Then test build_teams_payload(...) directly.
Option 2: If testing the task, run it inside a flow
If send_teams_alert is a Prefect task, test it through a flow:
from prefect import flow
@flow
def test_flow(webhook):
return send_teams_alert(
webhook=webhook,
title="Pipeline Failed",
message="COPY INTO failed",
facts=[
{"name": "Domain", "value": "demo"},
{"name": "Environment", "value": "test"},
],
status="error",
flow_run_url="<https://app.prefect.cloud/runs/123>",
)
def test_alert_formats_message_correctly(mock_teams_webhook):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch("<http://src.tasks.alerting.requests.post|src.tasks.alerting.requests.post>", return_value=mock_response) as mock_post:
result = test_flow(mock_teams_webhook)
assert result is True
Option 3: Disable/avoid API dependency in tests
If this should not need the API, set the test environment so Prefect does not try to use one.
For example, in the test:
```
from prefect.settings import PREFECT_API_URL, temporary_settings
def test_alert_formats_message_correctly(mock_teams_webhook):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with temporary_settings({PREFECT_API_URL: None}):
with patch("src.tasks.alerting.requests.post", return_value=mock_response) as mock_post:
result = send_teams_alert(Marvin
06/16/2026, 9:26 AMBut note: this only helps if your code can truly run *without* Prefect API features.
*Option 4: Mock the Prefect-specific calls*
If your function calls `get_run_logger()` or runtime APIs, mock those in the unit test.
Example:
with patch("src.tasks.alerting.get_run_logger") as mock_logger:
mock_logger.return_value = MagicMock()
...
```
Why CI fails but local passes
Locally you may have:
- a running Prefect server
- a valid profile
- a working ephemeral SQLite state
- environment vars already set
In GitHub Actions, none of that is guaranteed, so the hidden API dependency breaks.
Important clue from your traceback
The failure occurs before your <http://requests.post|requests.post> patch matters. That means something inside send_teams_alert touches Prefect before or during execution, not just the webhook request.
My strongest recommendation
For this specific test, don’t unit test the Prefect task object directly. Test the payload-building logic as plain Python, and keep one smaller integration test for the Prefect task/flow wrapper.
If you paste the actual send_teams_alert implementation, I can tell you exactly which line is triggering the Prefect client and show the minimal fix.André Lynum
06/16/2026, 9:30 AMMarvin
06/16/2026, 9:30 AMMarvin
06/16/2026, 9:31 AMsend_teams_alert is annotated with @task, then calling it directly can invoke Prefect task machinery, not just your function body. In your test environment, that machinery is trying to use a Prefect API at 127.0.0.1:8949, and that’s what is failing.
Why this breaks
A Prefect task is not just a plain function anymore. Depending on how it’s called, Prefect may:
- create task run state
- access runtime context
- initialize a client
- check API/server compatibility
So this:
result = send_teams_alert(...)
is not equivalent to calling a normal Python function.
Best practice
For unit tests, test the underlying function logic as plain Python.
For Prefect integration behavior, test the task inside a flow.
Two good options
Option 1: Split the logic from the task wrapper
This is the cleanest approach.
from prefect import task
import requests
def _send_teams_alert_impl(webhook, title, message, facts, status, flow_run_url=None):
payload = {
"title": title,
"text": message,
}
response = <http://requests.post|requests.post>(webhook.get_secret_value(), json=payload)
response.raise_for_status()
return True
@task
def send_teams_alert(webhook, title, message, facts, status, flow_run_url=None):
return _send_teams_alert_impl(
webhook=webhook,
title=title,
message=message,
facts=facts,
status=status,
flow_run_url=flow_run_url,
)
Then your unit test becomes:
def test_alert_formats_message_correctly(mock_teams_webhook):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch("<http://src.tasks.alerting.requests.post|src.tasks.alerting.requests.post>", return_value=mock_response) as mock_post:
result = _send_teams_alert_impl(
webhook=mock_teams_webhook,
title="Pipeline Failed",
message="COPY INTO failed",
facts=[
{"name": "Domain", "value": "demo"},
{"name": "Environment", "value": "test"},
],
status="error",
flow_run_url="<https://app.prefect.cloud/runs/123>",
)
assert result is True
Option 2: If you want to test the Prefect task, run it inside a flow
That gives the task proper runtime context.
from prefect import flow
@flow
def run_alert_task(webhook):
return send_teams_alert(
webhook=webhook,
title="Pipeline Failed",
message="COPY INTO failed",
facts=[
{"name": "Domain", "value": "demo"},
{"name": "Environment", "value": "test"},
],
status="error",
flow_run_url="<https://app.prefect.cloud/runs/123>",
)
def test_alert_formats_message_correctly(mock_teams_webhook):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
with patch("<http://src.tasks.alerting.requests.post|src.tasks.alerting.requests.post>", return_value=mock_response) as mock_post:
result = run_alert_task(mock_teams_webhook)
assert result is True
What I would not recommend
- Relying on direct task invocation for a unit test
- Depending on an ephemeral API server in GitHub Actions for a formatting test
- Working around this by standing up more Prefect infrastructure unless you truly want an integration test
Short version
Because @task wraps the function, your “unit test” is accidentally becoming a Prefect runtime/integration test. In CI, that causes a failing API call.
Recommended fix: move the message formatting + <http://requests.post|requests.post> into a plain Python helper and test that helper directly.
Docs:
- Prefect tasks
- Prefect docs
If you want, I can rewrite your test file into:
1. pure unit tests for payload formatting, and
2. one Prefect flow-based integration test.