asgion
ASGI protocol inspector and trace engine. Catch subtle protocol violations your tests miss — before they hit production.
Why asgion?
ASGI apps can pass all tests while still violating the protocol:
- Sending response body before
http.response.start - Writing to a closed WebSocket connection
- Exiting without completing a streaming response
- Returning malformed event payloads
Frameworks catch some of this. asgion validates the full ASGI contract — state machines, event schemas, scope fields, and semantic constraints across HTTP, WebSocket, and Lifespan.
Highlights
- Full ASGI contract validation — 163 rules across HTTP, WebSocket, and Lifespan
- Trace engine — record every
receive()/send()with nanosecond timestamps and inline violation markers - CI-ready — deterministic exit codes and JSON output
- CLI, Python API, pytest plugin — fits any workflow
- Zero runtime dependencies — pure Python 3.12+
- O(1) per message — safe for hot paths, no overhead when tracing is off
Works with any ASGI app: FastAPI, Starlette, Litestar, Django (ASGI), or bare ASGI handlers.
Quickstart
Get started in under a minute:
pip install asgion[cli]
Given a buggy app that sends body before http.response.start:
# myapp.py
async def app(scope, receive, send):
if scope["type"] == "http":
await send({"type": "http.response.body", "body": b"oops"}) # wrong order
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"hello"})
Check finds protocol violations:
asgion check myapp:app
CHECK myapp:app
── GET / ─────────────────────────────────────────────────────────
[HF-002] error: http.response.body sent without preceding http.response.start
hint: Send http.response.start before any http.response.body
[SEM-002] info: No Content-Type header on 2xx response
hint: Responses with a body should include a Content-Type header
2 violations (1 error, 1 info) | 709µs
What is HF-002? Look up any rule directly:
asgion rules HF-002
RULE [HF-002] error
http.response.body sent without preceding http.response.start
hint: Send http.response.start before any http.response.body
layer: http.fsm
applies to: http
Trace shows the full event timeline for the same app:
asgion trace myapp:app
TRACE GET / (88µs, TTFB 54µs)
26µs send http.response.body 4 bytes ← HF-002 (error)
54µs send http.response.start 200 (+28µs) ← SEM-002 (info)
75µs send http.response.body 5 bytes (+21µs)
Events: 3 | Violations: 2 (1 error, 1 info)
asgion trace myapp:app --min-severity error # only error-level markers
asgion trace myapp:app --out ./traces/ # save as JSON files
Filter, export, target specific endpoints:
asgion check myapp:app --select "HF-*" --min-severity warning
asgion check myapp:app --path /api/users --path "POST:/api/users" -H "Content-Type: application/json"
asgion check myapp:app --format sarif --out report.sarif # GitHub Code Scanning
asgion check myapp:app --format junit --out report.xml # Jenkins / GitLab CI
Bootstrap a config file:
asgion init # creates .asgion.toml with commented-out defaults
asgion init --pyproject # prints [tool.asgion] block to stdout
Exit codes: 0 = clean, 1 = violations (--strict), 2 = runtime error. See asgion check --help.
App exceptions and timeouts are reported but do not affect exit codes — only protocol violations do.
Note:
asgion check myapp:appimports your module and drives the ASGI app. All application dependencies (FastAPI, SQLAlchemy, etc.) must be installed in the same environment as asgion.
See the full rule list for all 163 rules and their descriptions.
Python API
from asgion import Inspector
inspector = Inspector(app)
# ... drive the app via httpx, TestClient, etc. ...
assert inspector.violations == [] # fails if any protocol violation occurred
With tracing:
inspector = Inspector(app, trace=True)
async with httpx.AsyncClient(transport=ASGITransport(inspector)) as client:
await client.get("/api/users")
record = inspector.traces[0]
record.scope.method # "GET"
record.scope.path # "/api/users"
record.summary.ttfb_ns # time to first byte (ns)
As middleware:
from asgion import inspect
uvicorn.run(inspect(app))
Pytest Plugin
Integrate protocol validation directly into your test suite:
pip install asgion[pytest]
async def test_my_app(asgi_inspect):
inspector = asgi_inspect(my_app)
async with httpx.AsyncClient(transport=ASGITransport(inspector)) as client:
await client.get("/users")
assert inspector.violations == []
pytest --asgi-strict # auto-check all tests
CI Integration
GitHub Action
# .github/workflows/asgion.yml
name: ASGI Check
on: [push, pull_request]
jobs:
asgion:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ack1d/asgion@v0
with:
app: myapp:app
strict: true
format: sarif
pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/ack1d/asgion
rev: v0.6.0 # update with: pre-commit autoupdate
hooks:
- id: asgion
args: [myapp:app, --strict] # args is required — specify your app target
pre-commit runs in an isolated virtualenv — asgion can only validate raw ASGI apps that have no external imports. For apps with dependencies, use the CLI or GitHub Action instead.
Configuration
Via pyproject.toml, .asgion.toml, or Python API.
See configuration docs and full rule list.
[tool.asgion]
profile = "recommended"
exclude_rules = ["SEM-006"]
Contributing
git clone https://github.com/ack1d/asgion.git
cd asgion
uv sync --group dev
uv run pytest
If you have Task installed: task check runs lint, typecheck, and tests.
License
MIT
Metadata
Release files for asgion 0.6.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| asgion-0.6.1.tar.gz | 49.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| asgion-0.6.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 116.2 kB
Release files / asgion-0.6.1.tar.gz
| Download URL | asgion-0.6.1.tar.gz |
|---|---|
| Size | 49.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58f9538a3d610b1da22f1bd2dc3e36ddb2da8e25d98777d0c676cf408a53fdd1
|
|
BLAKE2b-256 checksum How to use checksums |
4ffaecc954e18f03208331e0711334ef2d4f218c72439759380544cb367b721a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 16, 2026.
Transparency logRelease files / asgion-0.6.1-py3-none-any.whl
| Download URL | asgion-0.6.1-py3-none-any.whl |
|---|---|
| Size | 67.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b697477cdf3599a7c02753177834df0da3f8dd1a0fcd2c502ca9299665f07750
|
|
BLAKE2b-256 checksum How to use checksums |
c93c70123d20806d559c6ca117a5a1459284aaa994e62942caba9b868e1909b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 16, 2026.
Transparency log