Skip to content
This repository has been archived by the owner on May 10, 2021. It is now read-only.

Commit

Permalink
Merge branch 'expose-function-context' into main (#119)
Browse files Browse the repository at this point in the history
When a page is being SSR-ed by a Netlify Function, allow users to access
the function's event and context parameters. These can be accessed as a
property on the `req` object in all SSR-ed pages and in API routes:

- req.netlifyFunctionParams.event
- req.netlifyFunctionParams.context

```js
const Page = () => <p>Hello World!</p>;

export const getServerSideProps = async ({ req }) => {
  // Get event and context from Netlify Function
  const {
    netlifyFunctionParams: { event, context },
  } = req;

  // Access Netlify identity
  const { identity, user } = context.clientContext;

  // Modify callbackWaitsForEmptyEventLoop behavior
  context.callbackWaitsForEmptyEventLoop = false;

  // See how much time is remaining before function timeout
  const timeRemaining = context.getRemainingTimeInMillis();

  return {
    props: {},
  };
};

export default Page;
```

This allows users to access/leverage Netlify identity for their Next.js
page (see #20).
It also allows users to modify the callbackWaitsForEmptyEventLoop
behavior (see #66 (comment)).

Fixes #20
  • Loading branch information
FinnWoelm committed Dec 22, 2020
2 parents 5ffce26 + 5fa6c5a commit f49155d
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 8 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The plugin can be found on [npm here](https://www.npmjs.com/package/@netlify/plu
- [Preview Locally](#preview-locally)
- [Custom Netlify Redirects](#custom-netlify-redirects)
- [Custom Netlify Functions](#custom-netlify-functions)
- [Using Netlify Identity](#using-netlify-identity)
- [Caveats](#caveats)
- [Fallbacks for Pages with `getStaticPaths`](#fallbacks-for-pages-with-getstaticpaths)
- [Credits](#credits)
Expand Down Expand Up @@ -199,6 +200,52 @@ Currently, there is no support for redirects set in your `netlify.toml` file.
`next-on-netlify` creates one Netlify Function for each of your
SSR pages and API endpoints. It is currently not possible to create custom Netlify Functions. This feature is on our list to do.

#### Using Netlify Identity

You can use [Netlify Identity](https://docs.netlify.com/visitor-access/identity/) with `next-on-netlify`. For all pages with server-side rendering (getInitialProps*, getServerSideProps, and API routes), you can access the [clientContext object](https://docs.netlify.com/functions/functions-and-identity/#access-identity-info-via-clientcontext) via the `req` parameter.

For example:

```js
const Page = () => <p>Hello World!</p>;

export const getServerSideProps = async ({ req }) => {
// Get event and context from Netlify Function
const {
netlifyFunctionParams: { event, context },
} = req;

// Access Netlify identity
const { identity, user } = context.clientContext;

return {
props: {},
};
};

export default Page;
```

To access Netlify Identity from pages without server-side rendering, you can create a [Next API route](https://nextjs.org/docs/api-routes/introduction) that performs identity-related logic:

```js
export default async function getUser(req, res) {
// Get event and context from Netlify Function
const {
netlifyFunctionParams: { event, context },
} = req;

// Access Netlify identity
const { user } = context.clientContext;

// Respond with user object
res.json({ user });
}
```

\* Note that pages using getInitialProps are only server-side rendered on initial page load and not when the user navigates client-side between pages.


## Caveats

### Fallbacks for Pages with `getStaticPaths`
Expand Down
3 changes: 3 additions & 0 deletions cypress/fixtures/pages/api/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function context(req, res) {
res.json({ req, res });
}
11 changes: 11 additions & 0 deletions cypress/fixtures/pages/getServerSideProps/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Context = ({ context }) => <pre>{JSON.stringify(context, 2, " ")}</pre>;

export const getServerSideProps = async (context) => {
return {
props: {
context,
},
};
};

export default Context;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const WaitForEmptyEventLoop = () => <p>Successfully rendered page!</p>;

export const getServerSideProps = async ({ params, req }) => {
// Set up long-running process
const timeout = setTimeout(() => {}, 100000);

// Set behavior of whether to wait for empty event loop
const wait = String(params.wait).toLowerCase() === "true";
const { context: functionContext } = req.netlifyFunctionParams;
functionContext.callbackWaitsForEmptyEventLoop = wait;

return {
props: {},
};
};

export default WaitForEmptyEventLoop;
64 changes: 64 additions & 0 deletions cypress/integration/default_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,49 @@ describe("getInitialProps", () => {
});

describe("getServerSideProps", () => {
it("exposes function context on the req object", () => {
cy.visit("/getServerSideProps/context");

cy.get("pre")
.first()
.then((json) => {
const {
req: {
netlifyFunctionParams: { event, context },
},
} = JSON.parse(json.html());

expect(event).to.have.property("path", "/getServerSideProps/context");
expect(event).to.have.property("httpMethod", "GET");
expect(event).to.have.property("headers");
expect(event).to.have.property("multiValueHeaders");
expect(event).to.have.property("isBase64Encoded");
expect(context.done).to.be.undefined;
expect(context.getRemainingTimeInMillis).to.be.undefined;
expect(context).to.have.property("awsRequestId");
expect(context).to.have.property("callbackWaitsForEmptyEventLoop");
expect(context).to.have.property("clientContext");
});
});

it("can modify the callbackWaitsForEmptyEventLoop behavior", () => {
// netlify dev never waits on empty event loop
if (Cypress.env("DEPLOY") !== "local") {
cy.request({
url: "/getServerSideProps/wait-on-empty-event-loop/true",
failOnStatusCode: false,
// Functions time out after 10s, so we need to wait a bit
timeout: 15000,
}).then((response) => {
expect(response.status).to.eq(502);
expect(response.body).to.contain("Task timed out");
});
}

cy.visit("/getServerSideProps/wait-on-empty-event-loop/false");
cy.get("p").should("contain", "Successfully rendered page!");
});

context("with static route", () => {
it("loads TV shows", () => {
cy.visit("/getServerSideProps/static");
Expand Down Expand Up @@ -534,6 +577,27 @@ describe("API endpoint", () => {
cy.get("h1").should("contain", "Show #999");
cy.get("p").should("contain", "Flash Gordon");
});

it("exposes function context on the req object", () => {
cy.request("/api/context").then((response) => {
const {
req: {
netlifyFunctionParams: { event, context },
},
} = response.body;

expect(event).to.have.property("path", "/api/context");
expect(event).to.have.property("httpMethod", "GET");
expect(event).to.have.property("headers");
expect(event).to.have.property("multiValueHeaders");
expect(event).to.have.property("isBase64Encoded");
expect(context.done).to.be.undefined;
expect(context.getRemainingTimeInMillis).to.be.undefined;
expect(context).to.have.property("awsRequestId");
expect(context).to.have.property("callbackWaitsForEmptyEventLoop");
expect(context).to.have.property("clientContext");
});
});
});

describe("Preview Mode", () => {
Expand Down
10 changes: 9 additions & 1 deletion lib/templates/createRequestObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const http = require("http");
// Based on API Gateway Lambda Compat
// Source: https://github.com/serverless-nextjs/serverless-next.js/blob/master/packages/compat-layers/apigw-lambda-compat/lib/compatLayer.js

const createRequestObject = ({ event }) => {
const createRequestObject = ({ event, context }) => {
const {
requestContext = {},
path = "",
Expand Down Expand Up @@ -52,6 +52,14 @@ const createRequestObject = ({ event }) => {
req.rawHeaders = [];
req.headers = {};

// Expose Netlify Function event and callback on request object.
// This makes it possible to access the clientContext, for example.
// See: https://github.com/netlify/next-on-netlify/issues/20
// It also allows users to change the behavior of waiting for empty event
// loop.
// See: https://github.com/netlify/next-on-netlify/issues/66#issuecomment-719988804
req.netlifyFunctionParams = { event, context };

for (const key of Object.keys(multiValueHeaders)) {
for (const value of multiValueHeaders[key]) {
req.rawHeaders.push(key);
Expand Down
6 changes: 1 addition & 5 deletions lib/templates/netlifyFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ exports.handler = async (event, context, callback) => {
console.log("[request]", path);

// Render the Next.js page
const response = await renderNextPage({
...event,
// Required. Otherwise, reqResMapper will complain
requestContext: {},
});
const response = await renderNextPage({ event, context });

// Convert header values to string. Netlify does not support integers as
// header values. See: https://github.com/netlify/cli/issues/451
Expand Down
4 changes: 2 additions & 2 deletions lib/templates/renderNextPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ const createRequestObject = require("./createRequestObject");
const createResponseObject = require("./createResponseObject");

// Render the Next.js page
const renderNextPage = (event) => {
const renderNextPage = ({ event, context }) => {
// The Next.js page is rendered inside a promise that is resolved when the
// Next.js page ends the response via `res.end()`
const promise = new Promise((resolve) => {
// Create a Next.js-compatible request and response object
// These mock the ClientRequest and ServerResponse classes from node http
// See: https://nodejs.org/api/http.html
const req = createRequestObject({ event });
const req = createRequestObject({ event, context });
const res = createResponseObject({
onResEnd: (response) => resolve(response),
});
Expand Down

0 comments on commit f49155d

Please sign in to comment.