Skip to content

Add streaming support for Next.js App Router #37

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,25 @@ async def jobs(request):
return await render_nextjs_page(request)
```

#### Using `nextjs_page` with `stream=True` (Recommended)

If you're using the [Next.js App Router](https://nextjs.org/docs/app) (introduced in Next.js 13+), you can enable streaming by setting the `stream=True` parameter in the `nextjs_page` function. This allows the HTML response to be streamed directly from the Next.js server to the client. This approach is particularly useful for server-side rendering with streaming support to show an [instant loading state](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#instant-loading-states) from the Next.js server while the content of a route segment loads.

Here's an example:

```python
from django_nextjs.views import nextjs_page

urlpatterns = [
path("/nextjs/page", nextjs_page(stream=True), name="nextjs_page"),
]
```

**Considerations:**

- When using `stream_nextjs_page`, you cannot use a custom HTML template in Django, as the HTML is streamed directly from the Next.js server.
- The `stream` parameter will default to `True` in future releases. Currently, it is set to `False` for backward compatibility. To avoid breaking changes, we recommend explicitly setting `stream=False` if you are customizing HTML and do not want to use streaming.

## Customizing the HTML Response

You can modify the HTML code that Next.js returns in your Django code.
Expand Down
45 changes: 43 additions & 2 deletions django_nextjs/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import aiohttp
from asgiref.sync import sync_to_async
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
from django.middleware.csrf import get_token as get_csrf_token
from django.template.loader import render_to_string
from multidict import MultiMapping
Expand Down Expand Up @@ -70,14 +70,20 @@ def _get_nextjs_request_headers(request: HttpRequest, headers: Union[Dict, None]
}


def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict:
def _get_nextjs_response_headers(headers: MultiMapping[str], stream: bool = False) -> Dict:
return filter_mapping_obj(
headers,
selected_keys=[
"Location",
"Vary",
"Content-Type",
"Set-Cookie",
"Link",
"Cache-Control",
"Connection",
"Date",
"Keep-Alive",
*(["Transfer-Encoding"] if stream else []),
],
)

Expand Down Expand Up @@ -150,3 +156,38 @@ async def render_nextjs_page(
headers=headers,
)
return HttpResponse(content=content, status=status, headers=response_headers)


async def stream_nextjs_page(
request: HttpRequest,
allow_redirects: bool = False,
headers: Union[Dict, None] = None,
):
"""
Stream a Next.js page response.
This function is used to stream the response from a Next.js server.
"""
page_path = quote(request.path_info.lstrip("/"))
params = [(k, v) for k in request.GET.keys() for v in request.GET.getlist(k)]
next_url = f"{NEXTJS_SERVER_URL}/{page_path}"

session = aiohttp.ClientSession(
Copy link
Member

@mjnaderi mjnaderi Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using aiohttp.ClientSession and session.get as context manager (like the implementation of _render_nextjs_page_to_string).

From the documentation of aiohttp.ClientSession.request (link):

Returns a response object that should be used as an async context manager.

cookies=_get_nextjs_request_cookies(request),
headers=_get_nextjs_request_headers(request, headers),
)
nextjs_response = await session.get(next_url, params=params, allow_redirects=allow_redirects)
response_headers = _get_nextjs_response_headers(nextjs_response.headers, stream=True)

async def stream_nextjs_response():
try:
async for chunk in nextjs_response.content.iter_any():
yield chunk
finally:
await nextjs_response.release()
await session.close()

return StreamingHttpResponse(
stream_nextjs_response(),
status=nextjs_response.status,
headers=response_headers,
)
9 changes: 8 additions & 1 deletion django_nextjs/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
from typing import Dict, Union

from .render import render_nextjs_page
from .render import render_nextjs_page, stream_nextjs_page


def nextjs_page(
*,
stream: bool = False,
template_name: str = "",
context: Union[Dict, None] = None,
using: Union[str, None] = None,
allow_redirects: bool = False,
headers: Union[Dict, None] = None,
):
if stream and (template_name or context or using):
raise ValueError("When 'stream' is set to True, you should not use 'template_name', 'context', or 'using'")

async def view(request, *args, **kwargs):
if stream:
return await stream_nextjs_page(request=request, allow_redirects=allow_redirects, headers=headers)

return await render_nextjs_page(
request=request,
template_name=template_name,
Expand Down
Loading