Skip to content

Instantly share code, notes, and snippets.

@cramforce
Last active November 4, 2022 22:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cramforce/12e58aea2920ae80159f42be451e8027 to your computer and use it in GitHub Desktop.
Save cramforce/12e58aea2920ae80159f42be451e8027 to your computer and use it in GitHub Desktop.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const SCRIPT_TAG = '<script async src="/feedback-bootstrap.js"></script>';
export async function middleware(request: NextRequest) {
// Skip requests not looking for html.
const accept = request.headers.get("accept") || "";
if (!accept) {
console.log("Request headers", request.headers);
}
if (!accept || !accept.startsWith("text/html")) {
return NextResponse.next();
}
const middlewareResponse = NextResponse.next();
// Skip redirects, etc.
if (middlewareResponse.status > 200) {
return middlewareResponse;
}
// We want to change the response, and that means we have to fetch it ourselves.
const originResponse = await fetch(request);
// Response doesn't look like HTML, skip!
const contentType = originResponse.headers.get("content-type");
if (!contentType) {
console.log("Origin response headers", originResponse.headers);
}
if (!contentType || !contentType.startsWith("text/html")) {
return originResponse;
}
let rewritten;
if (originResponse.body.getReader) {
console.log("Using the streaming API");
const reader = originResponse.body.getReader();
rewritten = new ReadableStream({
start(controller) {
function push() {
return reader.read().then(({ done, value }) => {
if (done) {
// Append the script tag.
controller.enqueue(new TextEncoder().encode(SCRIPT_TAG));
controller.close();
return;
}
controller.enqueue(value);
push();
});
}
push();
},
});
} else {
// It appears that originResponse.body.getReader is null and so we cannot stream the
// addition.
const text = await originResponse.text();
// Dumping it to the end is simple and works.
rewritten = text + SCRIPT_TAG;
}
return new Response(rewritten, {
headers: new Headers(originResponse.headers),
status: originResponse.status,
statusText: originResponse.statusText,
});
}
@karaggeorge
Copy link

karaggeorge commented Mar 17, 2022

if (!request.headers.get("accept").startsWith("text/html")) {
return NextResponse.next();
}

This was my first attempt as well, unfortunately for Next.js at least, this will only handle the first page load. Any relative links clicked on the page will not get caught. I guess if the script is loaded and there's no full refresh that will still work, but there might be an edge-case there when we are tracking what page the comments are on.

if (middlewareResponse.status > 200) {
return middlewareResponse;
}

I don't think this will work. NextResponse.next() doesn't actually fetch anything. It just signifies to keep moving to the next middleware. It won't actually have any response data by the time this middleware has resolved. We can check this on the origResponse however

const rewritten =
text + '<script async src="/feedback-bootstrap.js"></script>';

Wonder if we can do this using the streams instead of getting the text, since middleware supports streaming. Just for a bit better performance

@cramforce
Copy link
Author

if (!request.headers.get("accept").startsWith("text/html")) {
return NextResponse.next();
}

This was my first attempt as well, unfortunately for Next.js at least, this will only handle the first page load. Any relative links clicked on the page will not get caught. I guess if the script is loaded and there's no full refresh that will still work, but there might be an edge-case there when we are tracking what page the comments are on.

I think that is actually exactly what we want. We do need to monitor when location.href changes and update the comment state.

if (middlewareResponse.status > 200) {
return middlewareResponse;
}

I don't think this will work. NextResponse.next() doesn't actually fetch anything. It just signifies to keep moving to the next middleware. It won't actually have any response data by the time this middleware has resolved. We can check this on the origResponse however

The idea is that if other middlewares do a redirect, then this one should do nothing. It would likely also check whether there is a body in case the middleware actually responded.

const rewritten =
text + '<script async src="/feedback-bootstrap.js"></script>';

Wonder if we can do this using the streams instead of getting the text, since middleware supports streaming. Just for a bit better performance

See comment. As far as I can see the streaming interface on the fetch response is broken. Otherwise we should absolutely do this.

@karaggeorge
Copy link

The idea is that if other middlewares do a redirect, then this one should do nothing. It would likely also check whether there is a body in case the middleware actually responded.

But NextResponse.next() doesn't actually run anything else. It's just a static object (so that check will always be false):

static next() {
    return new NextResponse(null, {
      headers: {
        'x-middleware-next': '1',
      },
    })
  }

It's used to check the response at the end of the middleware call to know what to do next. I think it's mostly meant to be used if you want to let the request fall through, but attach some extra headers.

As far as I can see the streaming interface on the fetch response is broken.

Yeah, it's a middleware issue, we have a workaround cleanResponseBody method in Live to get the correct ReadableStream (currently it returns a Node.js stream)

--

The rest looks good. Only worry here is if the user has an existing top-level middleware, I don't know how this will play out (how do we merge them)

This is pretty much exactly the way I implemented it locally as well, only difference was the streaming

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment