Skip to content

Commit

Permalink
Adapter enhancements (#9661)
Browse files Browse the repository at this point in the history
* quality of life updates for `App` (#9579)

* feat(app): writeResponse for node-based adapters

* add changeset

* Apply suggestions from code review

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Apply suggestions from code review

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* add examples for NodeApp static methods

* unexpose createOutgoingHttpHeaders from public api

* move headers test to core

* clientAddress test

* cookies test

* destructure renderOptions right at the start

---------

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Fallback node standalone to localhost (#9545)

* Fallback node standalone to localhost

* Update .changeset/tame-squids-film.md

* quality of life updates for the node adapter (#9582)

* descriptive names for files and functions

* update tests

* add changeset

* appease linter

* Apply suggestions from code review

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* `server-entrypoint.js` -> `server.js`

* prevent crash on stream error (from PR 9533)

* Apply suggestions from code review

Co-authored-by: Luiz Ferraz <luiz@lferraz.com>

* `127.0.0.1` -> `localhost`

* add changeset for fryuni's fix

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

---------

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Co-authored-by: Luiz Ferraz <luiz@lferraz.com>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* chore(vercel): delete request response conversion logic (#9583)

* refactor

* add changeset

* bump peer dependencies

* unexpose symbols (#9683)

* Update .changeset/tame-squids-film.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Arsh <69170106+lilnasy@users.noreply.github.com>
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Co-authored-by: Luiz Ferraz <luiz@lferraz.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
6 people committed Jan 17, 2024
1 parent 3a4d5ec commit d6edc75
Show file tree
Hide file tree
Showing 34 changed files with 706 additions and 789 deletions.
32 changes: 32 additions & 0 deletions .changeset/cool-foxes-talk.md
@@ -0,0 +1,32 @@
---
"astro": minor
---

Adds new helper functions for adapter developers.

- `Astro.clientAddress` can now be passed directly to the `app.render()` method.
```ts
const response = await app.render(request, { clientAddress: "012.123.23.3" })
```

- Helper functions for converting Node.js HTTP request and response objects to web-compatible `Request` and `Response` objects are now provided as static methods on the `NodeApp` class.
```ts
http.createServer((nodeReq, nodeRes) => {
const request: Request = NodeApp.createRequest(nodeReq)
const response = await app.render(request)
await NodeApp.writeResponse(response, nodeRes)
})
```

- Cookies added via `Astro.cookies.set()` can now be automatically added to the `Response` object by passing the `addCookieHeader` option to `app.render()`.
```diff
-const response = await app.render(request)
-const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse));

-if (setCookieHeaders.length) {
- for (const setCookieHeader of setCookieHeaders) {
- headers.append('set-cookie', setCookieHeader);
- }
-}
+const response = await app.render(request, { addCookieHeader: true })
```
7 changes: 7 additions & 0 deletions .changeset/early-cups-poke.md
@@ -0,0 +1,7 @@
---
"@astrojs/vercel": major
---

**Breaking**: Minimum required Astro version is now 4.2.0.
Reorganizes internals to be more maintainable.
---
7 changes: 7 additions & 0 deletions .changeset/tame-squids-film.md
@@ -0,0 +1,7 @@
---
'@astrojs/node': major
---

If host is unset in standalone mode, the server host will now fallback to `localhost` instead of `127.0.0.1`. When `localhost` is used, the operating system can decide to use either `::1` (ipv6) or `127.0.0.1` (ipv4) itself. This aligns with how the Astro dev and preview server works by default.

If you relied on `127.0.0.1` (ipv4) before, you can set the `HOST` environment variable to `127.0.0.1` to explicitly use ipv4. For example, `HOST=127.0.0.1 node ./dist/server/entry.mjs`.
5 changes: 5 additions & 0 deletions .changeset/unlucky-stingrays-clean.md
@@ -0,0 +1,5 @@
---
"@astrojs/node": patch
---

Fixes an issue where the preview server appeared to be ready to serve requests before binding to a port.
6 changes: 6 additions & 0 deletions .changeset/weak-apes-add.md
@@ -0,0 +1,6 @@
---
"@astrojs/node": major
---

**Breaking**: Minimum required Astro version is now 4.2.0.
Reorganizes internals to be more maintainable.
7 changes: 2 additions & 5 deletions examples/ssr/src/api.ts
Expand Up @@ -17,16 +17,13 @@ interface Cart {
}>;
}

function getOrigin(request: Request): string {
return new URL(request.url).origin.replace('localhost', '127.0.0.1');
}

async function get<T>(
incomingReq: Request,
endpoint: string,
cb: (response: Response) => Promise<T>
): Promise<T> {
const response = await fetch(`${getOrigin(incomingReq)}${endpoint}`, {
const origin = new URL(incomingReq.url).origin;
const response = await fetch(`${origin}${endpoint}`, {
credentials: 'same-origin',
headers: incomingReq.headers,
});
Expand Down
Expand Up @@ -4,8 +4,8 @@ import type { OutgoingHttpHeaders } from 'node:http';
* Takes in a nullable WebAPI Headers object and produces a NodeJS OutgoingHttpHeaders object suitable for usage
* with ServerResponse.writeHead(..) or ServerResponse.setHeader(..)
*
* @param webHeaders WebAPI Headers object
* @returns NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values
* @param headers WebAPI Headers object
* @returns {OutgoingHttpHeaders} NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values
*/
export const createOutgoingHttpHeaders = (
headers: Headers | undefined | null
Expand Down
88 changes: 77 additions & 11 deletions packages/astro/src/core/app/index.ts
Expand Up @@ -29,15 +29,46 @@ import { EndpointNotFoundError, SSRRoutePipeline } from './ssrPipeline.js';
import type { RouteInfo } from './types.js';
export { deserializeManifest } from './common.js';

const clientLocalsSymbol = Symbol.for('astro.locals');

const localsSymbol = Symbol.for('astro.locals');
const clientAddressSymbol = Symbol.for('astro.clientAddress');
const responseSentSymbol = Symbol.for('astro.responseSent');

const STATUS_CODES = new Set([404, 500]);
/**
* A response with one of these status codes will be rewritten
* with the result of rendering the respective error page.
*/
const REROUTABLE_STATUS_CODES = new Set([404, 500]);

export interface RenderOptions {
routeData?: RouteData;
/**
* Whether to automatically add all cookies written by `Astro.cookie.set()` to the response headers.
*
* When set to `true`, they will be added to the `Set-Cookie` header as comma-separated key=value pairs. You can use the standard `response.headers.getSetCookie()` API to read them individually.
*
* When set to `false`, the cookies will only be available from `App.getSetCookieFromResponse(response)`.
*
* @default {false}
*/
addCookieHeader?: boolean;

/**
* The client IP address that will be made available as `Astro.clientAddress` in pages, and as `ctx.clientAddress` in API routes and middleware.
*
* Default: `request[Symbol.for("astro.clientAddress")]`
*/
clientAddress?: string;

/**
* The mutable object that will be made available as `Astro.locals` in pages, and as `ctx.locals` in API routes and middleware.
*/
locals?: object;

/**
* **Advanced API**: you probably do not need to use this.
*
* Default: `app.match(request)`
*/
routeData?: RouteData;
}

export interface RenderErrorOptions {
Expand Down Expand Up @@ -160,11 +191,24 @@ export class App {
): Promise<Response> {
let routeData: RouteData | undefined;
let locals: object | undefined;
let clientAddress: string | undefined;
let addCookieHeader: boolean | undefined;

if (
routeDataOrOptions &&
('routeData' in routeDataOrOptions || 'locals' in routeDataOrOptions)
(
'addCookieHeader' in routeDataOrOptions ||
'clientAddress' in routeDataOrOptions ||
'locals' in routeDataOrOptions ||
'routeData' in routeDataOrOptions
)
) {
if ('addCookieHeader' in routeDataOrOptions) {
addCookieHeader = routeDataOrOptions.addCookieHeader;
}
if ('clientAddress' in routeDataOrOptions) {
clientAddress = routeDataOrOptions.clientAddress;
}
if ('routeData' in routeDataOrOptions) {
routeData = routeDataOrOptions.routeData;
}
Expand All @@ -178,7 +222,12 @@ export class App {
this.#logRenderOptionsDeprecationWarning();
}
}

if (locals) {
Reflect.set(request, localsSymbol, locals);
}
if (clientAddress) {
Reflect.set(request, clientAddressSymbol, clientAddress)
}
// Handle requests with duplicate slashes gracefully by cloning with a cleaned-up request URL
if (request.url !== collapseDuplicateSlashes(request.url)) {
request = new Request(collapseDuplicateSlashes(request.url), request);
Expand All @@ -189,7 +238,6 @@ export class App {
if (!routeData) {
return this.#renderError(request, { status: 404 });
}
Reflect.set(request, clientLocalsSymbol, locals ?? {});
const pathname = this.#getPathnameFromRequest(request);
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
const mod = await this.#getModuleForRoute(routeData);
Expand All @@ -206,7 +254,7 @@ export class App {
);
let response;
try {
let i18nMiddleware = createI18nMiddleware(
const i18nMiddleware = createI18nMiddleware(
this.#manifest.i18n,
this.#manifest.base,
this.#manifest.trailingSlash
Expand All @@ -233,16 +281,21 @@ export class App {
}
}

// endpoints do not participate in implicit rerouting
if (routeData.type === 'page' || routeData.type === 'redirect') {
if (STATUS_CODES.has(response.status)) {
if (REROUTABLE_STATUS_CODES.has(response.status)) {
return this.#renderError(request, {
response,
status: response.status as 404 | 500,
});
}
Reflect.set(response, responseSentSymbol, true);
return response;
}
if (addCookieHeader) {
for (const setCookieHeaderValue of App.getSetCookieFromResponse(response)) {
response.headers.append('set-cookie', setCookieHeaderValue);
}
}
Reflect.set(response, responseSentSymbol, true);
return response;
}

Expand All @@ -259,6 +312,19 @@ export class App {
return getSetCookiesFromResponse(response);
}

/**
* Reads all the cookies written by `Astro.cookie.set()` onto the passed response.
* For example,
* ```ts
* for (const cookie_ of App.getSetCookieFromResponse(response)) {
* const cookie: string = cookie_
* }
* ```
* @param response The response to read cookies from.
* @returns An iterator that yields key-value pairs as equal-sign-separated strings.
*/
static getSetCookieFromResponse = getSetCookiesFromResponse

/**
* Creates the render context of the current route
*/
Expand Down

0 comments on commit d6edc75

Please sign in to comment.