-
Notifications
You must be signed in to change notification settings - Fork 4k
[spec] st.App - ASGI application entry point
#13449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really love this spec, great work! Overall I agree with the direction here, left a few detail comments.
Few more generic questions:
- I think I agree that going with
st.Appmakes more sense, but I know in the past we also considered adding something likest.mount, which would work similar to Gradio'smount_gradio_app(see here). Did you consider that option? I guess the main advantage ofst.Appis that I can also directly modify the Starlette app object, without mounting it in a FastAPI app, right? - API-wise, instead of
st.Appwe could also consider calling the app/server objectstreamlit.Streamlit, similar tofastapi.FastAPI. But I think I agree thatst.Appsounds a bit nicer and more intuitive. - When do you plan to ship this? Do you first want to wait until we fully migrated to Starlette, or already ship this before? If we do ship it before, is there any risk we need to revert the Starlette migration?
|
|
||
| ```python | ||
| st.App( | ||
| script_path: str | Path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this also accept a function, similar to st.Page? Doesn't necessarily need to/don't see a super strong use case but would be nice a) to be consistent and b) for small toy apps, if you just want to try this out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our current runtime setup requires a (main) script path; supporting a function here as well might require quite a bit of refactoring. This is probably something to track as a follow-up.
| lifespan: Callable[[App], AsyncContextManager[dict[str, Any] | None]] | None = None, | ||
| routes: Sequence[BaseRoute] | None = None, | ||
| middleware: Sequence[Middleware] | None = None, | ||
| exception_handlers: Mapping[Any, ExceptionHandler] | None = None, | ||
| debug: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we explicitly add these parameters here or just have **kwargs that we pass on to the Starlette object? If we list the params explicitly, what happens if Starlette changes something? Or is that unlikely since it's such a fundamental API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or is that unlikely since it's such a fundamental API?
I would probably lean towards keeping it explicit since it seems quite unlikely to expect changes on the Starlette side anytime soon. The only change in the last 7 years was the deprecation of on_startup and on_shutdown in favour of lifespan. Btw. FastAPI is also explicitly exposing all the Starlette parameters in addition to the parameters relevant for FastAPI.
|
|
||
| An ASGI-compatible application object that can be: | ||
|
|
||
| - Auto-detected and run via `streamlit run app.py` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very cool. Since it's just using streamlit run then, does that mean it would also work on Community Cloud and potentially SiS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's just using streamlit run then, does that mean it would also work on Community Cloud and potentially SiS?
Yes, it's expected to work on Community Cloud and SiS as well. But some aspects might be blocked by the proxy setup. That's something to determine through manual testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, yeah we should definitely test that out! Lmk if you have results.
| # Serve generated site folder (#6195) | ||
| generated_site = StaticFiles(directory="./generated_docs", html=True) | ||
|
|
||
| app = st.App( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small clarification: Do I need to do app = st.App(...) for the auto-detection to work, or can I also simply do st.App(...) without assigning it to a variable, or name that variable differently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can support both; we just need a way to "detect" that without actually running the script (e.g., regex matching or AST parsing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK sounds good, yeah either way is fine I think, should just document it clearly.
| async def send_with_cookie(message): | ||
| if message["type"] == "http.response.start": | ||
| headers = list(message.get("headers", [])) | ||
| headers.append((b"set-cookie", b"my_cookie=value; Path=/; HttpOnly")) | ||
| message = {**message, "headers": headers} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there also a way to trigger the cookie setting from within the app? Or is this always just getting set when the server starts, or on every request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting a cookie from within an app session isn't possible with this since it requires JavaScript execution on the client-side. However, dynamically writing cookies is also a bit of an anti-pattern -> usage of local storage is usually the correct solution in most of those cases.
I think we need to better understand the main use cases behind #861, but some of the use cases where cookies actually make sense can be solved by setting it on a request basis and using st.context.cookies in the app session.
| streamlit_app = st.App("dashboard.py") | ||
|
|
||
| # Mount Streamlit alongside FastAPI | ||
| app.mount("/dashboard", streamlit_app) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this also be run with streamlit run? I guess not, because it doesn't contain an st.App object, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could theoretically support any Starlette object within the main script, since streamlit run is just a lightweight wrapper around Uvicorn to start an ASGI server. Which would also mean that you can fastapi app without any Streamlit endpoint with streamlit run
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. We should just make sure there's no way to abuse this on Community Cloud. Even though it's probably fine if a few people host FastAPI apps there, as long as it doesn't become a regular pattern.
| **App discovery:** | ||
|
|
||
| When `streamlit run app.py` is invoked, Streamlit checks if the script contains an | ||
| `st.App` instance (similar to [FastAPI CLI discovery](https://github.com/fastapi/fastapi-cli/blob/main/src/fastapi_cli/discover.py)) by checking for a `st.App` instance named `app`. If no `st.App` instance is found, Streamlit will run the script in traditional mode. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess this means if an st.App instance is found, it will just run that script once and not rerun it in any way, right? Only the script passed to the script_path parameter is rerun in the normal Streamlit way, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, how does this work during development? If I change the script containing st.App, will the app automatically reload/rerun when I'm changing the script?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess this means if an st.App instance is found, it will just run that script once and not rerun it in any way, right? Only the script passed to the script_path parameter is rerun in the normal Streamlit way, right?
yep 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, how does this work during development? If I change the script containing st.App, will the app automatically reload/rerun when I'm changing the script?
This will need some more investigation, but uvicorn also has auto-reloading capabilities that could be used for changes to the st.App script.
I think going with
Yep, that would also work. But I think
I'm trying to implement such an interface already in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a product specification for a new st.App class that will provide an ASGI-compatible entry point for Streamlit applications. The specification follows up on the ongoing Starlette support implementation and aims to address numerous community feature requests totaling over 400 upvotes.
Key changes:
- Proposes an ASGI application entry point with Starlette-compatible API
- Enables custom HTTP routes, middleware configuration, and lifecycle hooks
- Supports integration with popular Python web frameworks (Django, FastAPI)
specs/0000-st-app/product-spec.md
Outdated
| | No breaking API changes | ✅ | | ||
| | No new dependencies | ✅ will already added in Starlette migration | | ||
| | Metrics collected | We need to track the st.App usage via a flag in the metrics. | | ||
| | Any security/legal impact? | ✅ no new implicatations besides whats relevant for Starlette migration | |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phrase "whats relevant" is missing an apostrophe. It should be "what's relevant".
| | Any security/legal impact? | ✅ no new implicatations besides whats relevant for Starlette migration | | |
| | Any security/legal impact? | ✅ no new implicatations besides what's relevant for Starlette migration | |
| headers = dict(message.get("headers", [])) | ||
| # Add security headers (#6417, #9160) | ||
| headers[b"x-frame-options"] = b"SAMEORIGIN" | ||
| headers[b"x-content-type-options"] = b"nosniff" | ||
| headers[b"strict-transport-security"] = b"max-age=31536000" | ||
| headers[b"content-security-policy"] = b"default-src 'self'" | ||
| message["headers"] = list(headers.items()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The middleware implementation will lose duplicate HTTP headers. Converting headers from ASGI's list-of-tuples format [(b'name', b'value'), ...] to a dict and back will drop duplicate header names, which are valid in HTTP (e.g., multiple Set-Cookie headers). This will break applications that set multiple headers with the same name.
# Fix: Directly append to the list instead of dict conversion
async def send_with_headers(message):
if message["type"] == "http.response.start":
headers = list(message.get("headers", []))
headers.extend([
(b"x-frame-options", b"SAMEORIGIN"),
(b"x-content-type-options", b"nosniff"),
(b"strict-transport-security", b"max-age=31536000"),
(b"content-security-policy", b"default-src 'self'")
])
message["headers"] = headers
await send(message)| headers = dict(message.get("headers", [])) | |
| # Add security headers (#6417, #9160) | |
| headers[b"x-frame-options"] = b"SAMEORIGIN" | |
| headers[b"x-content-type-options"] = b"nosniff" | |
| headers[b"strict-transport-security"] = b"max-age=31536000" | |
| headers[b"content-security-policy"] = b"default-src 'self'" | |
| message["headers"] = list(headers.items()) | |
| headers = list(message.get("headers", [])) | |
| # Add security headers (#6417, #9160) | |
| headers.extend([ | |
| (b"x-frame-options", b"SAMEORIGIN"), | |
| (b"x-content-type-options", b"nosniff"), | |
| (b"strict-transport-security", b"max-age=31536000"), | |
| (b"content-security-policy", b"default-src 'self'") | |
| ]) | |
| message["headers"] = headers |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| |------|---------------| | ||
| | Works on SiS, Cloud, etc? | ⚠️ Likely, but will need testing. | | ||
| | No breaking API changes | ✅ | | ||
| | No new dependencies | ✅ will already added in Starlette migration | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar error: "will already added in" should be "will already be added in" or "already added in".
# Current:
| No new dependencies | ✅ will already added in Starlette migration |
# Fixed:
| No new dependencies | ✅ will already be added in Starlette migration || | No new dependencies | ✅ will already added in Starlette migration | | |
| | No new dependencies | ✅ will already be added in Starlette migration | |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
jrieke
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds great, thanks for the answers! Approving.
…on (#13537) ## Describe your changes Add a new experimental `App` class that provides an ASGI-compatible entry point for Streamlit apps. This enables custom HTTP routes, middleware configuration, lifecycle hooks, and integration with popular Python web frameworks, aligning Streamlit with the broader async web ecosystem. See the spec for more details: #13449 The `App` instance is exposed via `from streamlit.starlette import App` but will likely be moved into the `st` namespace once we move this out of experimental. This PR also includes the discovery logic for detecting if the main script contains an App call -> which triggers a special asgi execution mode. ## GitHub Issue Link (if applicable) - Spec: #13449 - #439 - #9673 - #6195 - #9090 - #11333 - #8713 - #6417 - #9160 - #861 - #8823 - #4311 - #4567 - #927 - #8661 - #7546 - #7688 - #9916 - #8991 - #6108 - #8545 - #7667 - #5673 - #8999 ## Testing Plan - Added unit tests. --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.
Rendered spec: https://issues.streamlit.app/spec_renderer?pr=13449
GitHub Issue Link (if applicable)
Open Questions
Contribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.