From 91de2bee64f803d011521769db0f55a05485cdca Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Mon, 31 Mar 2025 16:29:14 +0330 Subject: [PATCH 01/11] Add streaming support for Next.js page responses --- django_nextjs/render.py | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index 2b534c5..6b09ef2 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -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 @@ -78,6 +78,12 @@ def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict: "Vary", "Content-Type", "Set-Cookie", + "Link", + "Transfer-Encoding", + "Cache-Control", + "Connection", + "Date", + "Keep-Alive", ], ) @@ -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, + headers: Union[Dict, None] = None, + allow_redirects: bool = False, +): + """ + 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( + 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) + + 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, + ) From 83ecbf2c0f441d922c849af4d43f33522dd5e84b Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Mon, 31 Mar 2025 16:44:09 +0330 Subject: [PATCH 02/11] Update README.md to include usage instructions for stream_nextjs_page --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 458b81b..76bfb77 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,34 @@ async def jobs(request): return await render_nextjs_page(request) ``` +#### Using `stream_nextjs_page` with Next.js App Router + +If you're using the [Next.js App Router](https://nextjs.org/docs/app) (introduced in Next.js 13+), you can use the `stream_nextjs_page` function to stream the HTML response directly from Next.js 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 of how to use it: + +```python +from django_nextjs.render import stream_nextjs_page + +async def my_page(request): + # Your custom logic (if needed) + return await stream_nextjs_page(request) +``` + +**Note:** 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. + +Add the view to your `urlpatterns`: + +```python +from django.urls import path + +urlpatterns = [ + path("/my-page", my_page, name="my_page"), +] +``` + +This method ensures compatibility with the App Router's streaming capabilities while maintaining seamless integration with Django. + ## Customizing the HTML Response You can modify the HTML code that Next.js returns in your Django code. From 3d451fdd9fea6cff2c62d485b98abc41dc20a4e0 Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Thu, 10 Apr 2025 01:26:15 +0330 Subject: [PATCH 03/11] Add the Transfer-Encoding header to the response conditionally, based on whether the response is a stream or not --- django_nextjs/render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index 6b09ef2..5eb9bd4 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -70,7 +70,7 @@ 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=[ @@ -79,11 +79,11 @@ def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict: "Content-Type", "Set-Cookie", "Link", - "Transfer-Encoding", "Cache-Control", "Connection", "Date", "Keep-Alive", + "Transfer-Encoding" if stream else None, ], ) @@ -160,8 +160,8 @@ async def render_nextjs_page( async def stream_nextjs_page( request: HttpRequest, - headers: Union[Dict, None] = None, allow_redirects: bool = False, + headers: Union[Dict, None] = None, ): """ Stream a Next.js page response. @@ -176,7 +176,7 @@ async def stream_nextjs_page( 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) + response_headers = _get_nextjs_response_headers(nextjs_response.headers, stream=True) async def stream_nextjs_response(): try: From 6c64a8f0a7b70a103fa57952cb885099c5c62b9a Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Thu, 10 Apr 2025 01:26:40 +0330 Subject: [PATCH 04/11] Add streaming support to nextjs_page function with validation for parameters --- django_nextjs/views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/django_nextjs/views.py b/django_nextjs/views.py index 59024c5..d7b72db 100644 --- a/django_nextjs/views.py +++ b/django_nextjs/views.py @@ -1,17 +1,26 @@ 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' parameter is True, 'template_name', 'context', and 'using' cannot be used together." + ) + 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, From 3d2dae2a2fabc0e40fed85096b9835ab73de22be Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Thu, 10 Apr 2025 16:15:46 +0330 Subject: [PATCH 05/11] Update README.md to recommend using nextjs_page with stream=True for Next.js App Router --- README.md | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 76bfb77..ed6f564 100644 --- a/README.md +++ b/README.md @@ -161,33 +161,24 @@ async def jobs(request): return await render_nextjs_page(request) ``` -#### Using `stream_nextjs_page` with Next.js App Router +#### 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 use the `stream_nextjs_page` function to stream the HTML response directly from Next.js 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. +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 of how to use it: +Here's an example: ```python -from django_nextjs.render import stream_nextjs_page - -async def my_page(request): - # Your custom logic (if needed) - return await stream_nextjs_page(request) -``` - -**Note:** 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. - -Add the view to your `urlpatterns`: - -```python -from django.urls import path +from django_nextjs.views import nextjs_page urlpatterns = [ - path("/my-page", my_page, name="my_page"), + path("/nextjs/page", nextjs_page(stream=True), name="nextjs_page"), ] ``` -This method ensures compatibility with the App Router's streaming capabilities while maintaining seamless integration with Django. +**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 is currently set to `False` by default for backward compatibility. However, in future releases, it will be set to `True` by default. We recommend updating your code to explicitly enable streaming to prepare for this change. ## Customizing the HTML Response From 914a3ef185352a440e98d43b9eaca123fd7957f2 Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Thu, 17 Apr 2025 23:43:30 +0330 Subject: [PATCH 06/11] Update error message for stream parameter in nextjs_page function for clarity --- django_nextjs/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/django_nextjs/views.py b/django_nextjs/views.py index d7b72db..a431f9a 100644 --- a/django_nextjs/views.py +++ b/django_nextjs/views.py @@ -13,9 +13,7 @@ def nextjs_page( headers: Union[Dict, None] = None, ): if stream and (template_name or context or using): - raise ValueError( - "When 'stream' parameter is True, 'template_name', 'context', and 'using' cannot be used together." - ) + 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: From a04b8a0470f1fa8f9a2c6b10f858c06e59832b77 Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Thu, 17 Apr 2025 23:44:05 +0330 Subject: [PATCH 07/11] Refactor Transfer-Encoding header handling in response headers for clarity --- django_nextjs/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index 5eb9bd4..ccde251 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -83,7 +83,7 @@ def _get_nextjs_response_headers(headers: MultiMapping[str], stream: bool = Fals "Connection", "Date", "Keep-Alive", - "Transfer-Encoding" if stream else None, + *(["Transfer-Encoding"] if stream else []), ], ) From f3b03eaceb267b31d34295e480d99ebf1cf976d2 Mon Sep 17 00:00:00 2001 From: Omid Kazemi Date: Fri, 18 Apr 2025 00:02:03 +0330 Subject: [PATCH 08/11] Update README.md to clarify default behavior of stream parameter in future releases --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed6f564..8abad35 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ urlpatterns = [ **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 is currently set to `False` by default for backward compatibility. However, in future releases, it will be set to `True` by default. We recommend updating your code to explicitly enable streaming to prepare for this change. +- 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 From 7ec5a6c9c485a8e21937d5c46f7ef78a9f0a94bf Mon Sep 17 00:00:00 2001 From: Mohammad Javad Naderi Date: Mon, 28 Apr 2025 15:31:13 +0330 Subject: [PATCH 09/11] Improve session management and error handling in stream_nextjs_page --- django_nextjs/render.py | 48 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index ccde251..d5047d0 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -171,23 +171,31 @@ async def stream_nextjs_page( 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( - 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, - ) + session = aiohttp.ClientSession() + + try: + nextjs_response = await session.get( + next_url, + params=params, + allow_redirects=allow_redirects, + cookies=_get_nextjs_request_cookies(request), + headers=_get_nextjs_request_headers(request, headers) + ) + 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, + ) + except: + await session.close() + raise From 13dd736cd66e2a761bea19293bab59b9f6520aec Mon Sep 17 00:00:00 2001 From: Mohammad Javad Naderi Date: Mon, 28 Apr 2025 18:28:35 +0330 Subject: [PATCH 10/11] Remove Transfer-Encoding header --- django_nextjs/render.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index d5047d0..cbd29ce 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -70,7 +70,7 @@ def _get_nextjs_request_headers(request: HttpRequest, headers: Union[Dict, None] } -def _get_nextjs_response_headers(headers: MultiMapping[str], stream: bool = False) -> Dict: +def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict: return filter_mapping_obj( headers, selected_keys=[ @@ -83,7 +83,6 @@ def _get_nextjs_response_headers(headers: MultiMapping[str], stream: bool = Fals "Connection", "Date", "Keep-Alive", - *(["Transfer-Encoding"] if stream else []), ], ) @@ -181,7 +180,7 @@ async def stream_nextjs_page( cookies=_get_nextjs_request_cookies(request), headers=_get_nextjs_request_headers(request, headers) ) - response_headers = _get_nextjs_response_headers(nextjs_response.headers, stream=True) + response_headers = _get_nextjs_response_headers(nextjs_response.headers) async def stream_nextjs_response(): try: From 9470cd389741fd7909cf861c9fedcb7ee50c41ca Mon Sep 17 00:00:00 2001 From: Mohammad Javad Naderi Date: Mon, 28 Apr 2025 18:42:32 +0330 Subject: [PATCH 11/11] Fix formatting --- django_nextjs/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_nextjs/render.py b/django_nextjs/render.py index cbd29ce..22c593d 100644 --- a/django_nextjs/render.py +++ b/django_nextjs/render.py @@ -178,7 +178,7 @@ async def stream_nextjs_page( params=params, allow_redirects=allow_redirects, cookies=_get_nextjs_request_cookies(request), - headers=_get_nextjs_request_headers(request, headers) + headers=_get_nextjs_request_headers(request, headers), ) response_headers = _get_nextjs_response_headers(nextjs_response.headers)