/next-fetch-har

Debug Next.js SSR requests using HTTP Archive (HAR) logs

Primary LanguageJavaScriptMIT LicenseMIT

next-fetch-har

$ yarn add next-fetch-har

Demo

Motivation

When you click from page to page in a Next.js app, it’s easy to see and debug what API calls your getInitialProps methods are making: just look in your browser’s Network tab.

But what about during Server Side Rendering (SSR)? In my experience, one of the harder aspects of debugging Next.js apps is getting visibility into what getInitialProps is doing on the server – particularly what API requests and responses the app is seeing.

Usually, this is where good logging comes in. It’s very useful to have an HTTP client that can be instrumented with detailed logging so that you can see what requests are being made and whether they were successful. But how much can you reasonably log without spamming your console? All the headers? Large response bodies? What about timing information, like how long it took to receive each response?

Even if you connect to the Node.js Inspector, there is no server-side equivalent to the browser’s Network tab. So what if you had some other way to populate the Network tab with the network activity from the server, as if those requests were made in the browser?

That’s what node-fetch-har and this library allow you to do!

Usage

Step 1: Wrap your <App>

This library exports a withFetchHar Higher Order Component (HOC) that you can wrap your <App> component with to enable recording of server-side Fetch API calls.

In _app.js:

import App from "next/app";
import { withFetchHar } from "next-fetch-har";

export default withFetchHar(App);

Or with a custom <App>:

class CustomApp extends App {
  // Your customizations...
}

export default withFetchHar(CustomApp);

If you haven’t exposed a global fetch polyfill on the server, you can supply your fetch instance to the HOC:

import fetch from "isomorphic-unfetch";

export default withFetchHar(App, { fetch });

Step 2: Use the enhanced Fetch

Instead of using a global Fetch instance, the withFetchHar HOC creates a per-request instance of Fetch that logs requests as HAR entries. It adds this Fetch instance to the ctx object that your pages receive in getInitialProps. You should switch your calls to use this instance of Fetch.

class HomePage extends React.Component {
  static async getInitialProps(ctx) {
    // Get `fetch` from `ctx`.
    const { fetch } = ctx;

    // Example of what you might do with your API...
    const response = await fetch("/api/foo");
    const body = await response.json();

    return { value: body.foo };
  }

  render() {
    // Do something with value...
  }
}

Step 3: Download the HTTP Archive

When getInitialProps is complete, the withFetchHar HOC adds the resulting HTTP Archive (HAR) to the app’s getInitialProps output, and renders a download button at the bottom of the page to access it.

Demo

Enable in certain environments

You probably don’t want to enable HAR logging for every request in production (or if you do, it should probably only happen for superusers). Remember that HAR logs can contain sensitive information like passwords and cookie values!

The withFetchHar HOC has an enabled option you can use to conditionally enable HAR logging. When disabled, ctx.fetch will be the vanilla Fetch instance instead of the enhanced one.

withFetchHar(App, {
  // For safety, this is the default!
  enabled: process.env.NODE_ENV !== "production"
});

If you have multiple environments that run production builds of the app (i.e. a staging server), you can base this check on a different condition. For example:

withFetchHar(App, {
  enabled: process.env.DEPLOY_ENV !== "production"
});

You can also supply a function, which will be passed the same ctx object received by getInitialProps. You can use this to inspect req (or other properties) for special superuser status or other conditions:

withFetchHar(App, {
  enabled: ctx => ctx.req.user.isAdmin
});

Troubleshooting

Some browser APIs like Headers, URLSearchParams, etc. are not found!

node-fetch-har may require access to some of the browser APIs that Fetch uses. While node-fetch does export these, they aren’t necessarily globally available; some libraries like isomorphic-fetch set them on the global object for you.

You have two options:

  • Make them available on global yourself or using a library.

  • Pass them as options to withFetchHar, for example:

    import { Response } from "node-fetch";
    
    export default withFetchHar(App, { Response });

What if I don’t call fetch directly in my getInitialProps?

You’ll need to find some way to pass the ctx.fetch instance through to your code that needs to call it. It’s not possible to expose the enhanced HAR-enabled Fetch instance globally because it is scoped per request: it should only capture the requests made by each page, even if multiple are being served in parallel.

For example, if you make API calls inside Redux actions using a side-effect solution like redux-thunk, you can pass ctx.fetch to your store creation function so that it can be supplied to any side-effect middleware.

For redux-thunk in particular, you can use its withExtraArgument feature to pass a custom object containing fetch and whatever else you like:

function actionCreator() {
  return async (dispatch, getState, context) => {
    const response = await context.fetch("/api/foo");
  };
}

For scenarios where your store may sometimes have been created by getInitialProps and other times not, you can always fall back to vanilla Fetch, since we can only capture activity during getInitialProps anyway:

const store = createStore(
  reducer,
  applyMiddleware(
    thunk.withExtraArgument({
      fetch: ctx ? ctx.fetch : global.fetch
    })
  )
);

How do I prevent this from disabling automatic prerendering?

Next.js’ automatic prerendering feature is disabled if your custom <App> component defines a getInitialProps method. For most advanced apps, this is often inevitable, but if this library is the only thing causing your app to have getInitialProps, then you might want to work around it.

Using the enabled option is not good enough to prevent the resulting <App> from having getInitialProps, since ctx.fetch needs to be defined even when HAR logging is disabled (for your app to work whether enabled or disabled).

So, you’ll need to conditionally apply withFetchHar yourself and use a fallback wherever you access ctx.fetch:

let CustomApp = App;

if (process.env.NODE_ENV !== "production") {
  CustomApp = withFetchHar(CustomApp);
}

export default CustomApp;

In pages:

static async getInitialProps({ fetch = global.fetch }) {
  const response = await fetch('/api/foo');
}