$ yarn add next-fetch-har
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!
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 });
If you’d like to enable recording only for certain pages, the HOC also works with individual Page components:
function MyPage() {
// Your page content...
}
export default withFetchHar(MyPage);
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...
}
}
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.
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
});
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 });
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
})
)
);
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');
}
If you are only interested in recording requests for certain pages, you may
also use the withFetchHar
only on those page components, and prerendering
should not be affected on your other pages.