Passing variables from middleware to page in Next.js 12 new middleware api - next.js

Background to the Question
Vercel recently released their biggest update ever to Next.js. Next.js blog.
They introduced a lot of new features but my favorite is Middleware which:
"enables you to use code over configuration. This gives you full
flexibility in Next.js because you can run code before a request is
completed. Based on the user's incoming request, you can modify the
response by rewriting, redirecting, adding headers, or even streaming
HTML."
The Question
The following structure is used in this question.
- /pages
index.js
signin.js
- /app
_middleware.js # Will run before everything inside /app folder
index.js
The two important files here are /app/_middleware.js and /app/index.js.
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next();
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}
// /app/index.js
export default function Home() {
return (
<div>
<h1>Authenticated!</h1>
// session.firstName needs to be passed to this file from middleware
<p>Hello, { session.firstName }</p>
</div>
);
}
In this example /app/index.js needs access to the res.session JSON data. Is it possible to pass it in the NextResponse.next() function or do you need to do something else?
In express you can do res.locals.session = res.session

According to the examples (look specifically at /pages/_middleware.ts and /lib/auth.ts) it looks like the canonical way to do this would be to set your authentication via a cookie.
In your middleware function, that would look like:
// /app/_middleware.js
import { NextResponse } from 'next/server';
export function middleware(req, event) {
const res = { isSignedIn: true, session: { firstName: 'something', lastName: 'else' } }; // This "simulates" a response from an auth provider
if (res.isSignedIn) {
// Continue to /app/index.js
return NextResponse.next().cookie("cookie_key", "cookie_value"); // <--- SET COOKIE
} else {
// Redirect user
return NextResponse.redirect('/signin');
}
}

There's a another way but just like using cookie to achieve this. Just pass you data through headers.
// middleware.ts
async function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-HEADER', 'some-value-to-pass');
return response;
}
// _app.ts
function MyApp({ data }) {
// you can access your data here
<div>{data}</div>
}
MyApp.getInitialProps = ({ ctx }) => {
const data = ctx.res.getHeader('X-HEADER');
ctx.res.removeHeader('X-HEADER');
return { data };
};

Only weird solution is to inject your custom object into req.body because next.js v12 middleware doesn't allow altering the NextApiRequest
export const middleware = async (req: NextApiRequest) => {
// return new Response("Hello, world!");
req.body = { ...req.body, foo: "bar" };
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await middleware(req);
// now req.body.foo=='bar'
}
They do however explain how you can extend middleware here, but the example given (copied below) isn't meaningful enough because it doesnt show how withFoo() is implemented
import { NextApiRequest, NextApiResponse } from 'next'
import { withFoo } from 'external-lib-foo'
type NextApiRequestWithFoo = NextApiRequest & {
foo: (bar: string) => void
}
const handler = (req: NextApiRequestWithFoo, res: NextApiResponse) => {
req.foo('bar') // we can now use `req.foo` without type errors
res.end('ok')
}
export default withFoo(handler)
I assumed based on the above, withFoo.ts should be like this. But still wasn't successful in accessing request.Foo()
import { NextApiHandler, NextApiRequest } from "next";
export const withFoo = (handler: NextApiHandler) => {
//do stuff
};
Maybe someone can chip in?

We found a solution for 12.2+ middleware - published here:
https://clerk.dev/blog/nextjs-pass-value-from-middleware-to-api-routes-and-getserversideprops
And copying here for posterity...
Usage: middleware.js
import { NextResponse } from "next/server";
import { withContext } from "./context";
// Pre-define the possible context keys to prevent spoofing
const allowedContextKeys = ["foo"];
export default withContext(allowedContextKeys, (setContext, req) => {
setContext("foo", "bar");
return NextResponse.next();
});
Usage: API route (Node)
import { getContext } from "../../context";
export default function handler(req, res) {
res.status(200).json({ foo: getContext(req, "foo") });
}
Usage: API route (Edge)
import { getContext } from "../../context";
export default function handler(req) {
return new Response(JSON.stringify({ foo: getContext(req, "foo") }));
}
Usage: getServerSideProps (Edge and Node)
import { getContext } from "../context";
export const getServerSideProps = ({ req }) => {
return { props: { foo: getContext(req, "foo") } };
};
Source: (saved to context.js on your root)
import { NextResponse } from "next/server";
const ctxKey = (key) => `ctx-${key.toLowerCase()}`;
export const getContext = (req, rawKey) => {
const key = ctxKey(rawKey);
let headerValue =
typeof req.headers.get === "function"
? req.headers.get(key) // Edge
: req.headers[key]; // Node;
// Necessary for node in development environment
if (!headerValue) {
headerValue = req.socket?._httpMessage?.getHeader(key);
}
if (headerValue) {
return headerValue;
}
// Use a dummy url because some environments only return
// a path, not the full url
const reqURL = new URL(req.url, "http://dummy.url");
return reqURL.searchParams.get(key);
};
export const withContext = (allowedKeys, middleware) => {
// Normalize allowed keys
for (let i = 0; i < allowedKeys.length; i++) {
if (typeof allowedKeys[i] !== "string") {
throw new Error("All keys must be strings");
}
allowedKeys[i] = ctxKey(allowedKeys[i]);
}
return (req, evt) => {
const reqURL = new URL(req.url);
// First, make sure allowedKeys aren't being spoofed.
// Reliably overriding spoofed keys is a tricky problem and
// different hosts may behave different behavior - it's best
// just to safelist "allowedKeys" and block if they're being
// spoofed
for (const allowedKey of allowedKeys) {
if (req.headers.get(allowedKey) || reqURL.searchParams.get(allowedKey)) {
throw new Error(
`Key ${allowedKey.substring(
4
)} is being spoofed. Blocking this request.`
);
}
}
const data = {};
const setContext = (rawKey, value) => {
const key = ctxKey(rawKey);
if (!allowedKeys.includes(key)) {
throw new Error(
`Key ${rawKey} is not allowed. Add it to withContext's first argument.`
);
}
if (typeof value !== "string") {
throw new Error(
`Value for ${rawKey} must be a string, received ${typeof value}`
);
}
data[key] = value;
};
let res = middleware(setContext, req, evt) || NextResponse.next();
// setContext wasn't called, passthrough
if (Object.keys(data).length === 0) {
return res;
}
// Don't modify redirects
if (res.headers.get("Location")) {
return res;
}
const rewriteURL = new URL(
res.headers.get("x-middleware-rewrite") || req.url
);
// Don't modify cross-origin rewrites
if (reqURL.origin !== rewriteURL.origin) {
return res;
}
// Set context directly on the res object (headers)
// and on the rewrite url (query string)
for (const key in data) {
res.headers.set(key, data[key]);
rewriteURL.searchParams.set(key, data[key]);
}
// set the updated rewrite url
res.headers.set("x-middleware-rewrite", rewriteURL.href);
return res;
};
};

Related

404 page in nextjs app dir constantly refreshes with middleware

within my project, we use the nextjs app directory
however with that, it does not include root 404 pages, so we instead use the pages/404.tsx file. in our middleware, it uses next-auth and vercel edge config to send the user to a maintenance page (if enabled on edge config) and to check for auth pages and such, but even with everything commented out, it makes the 404 page constantly refresh itself
middleware.ts:
import { getToken } from "next-auth/jwt";
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { get } from "#vercel/edge-config";
export const config = {
matcher: ["/((?!_next|icon.svg|favicon.ico|api).*)"]
};
export default withAuth(
async function middleware(req: NextRequest) {
// if (process.env.EDGE_CONFIG && process.env.NODE_ENV !== "development") {
// if (req.nextUrl.pathname.startsWith("/maintenance")) return null;
// const isInMaintenanceMode = await get("isInMaintenanceMode");
// console.log("isInMaintenanceMode", isInMaintenanceMode);
// if (isInMaintenanceMode) {
// return NextResponse.redirect(new URL("/maintenance", req.url));
// }
// }
// if (req.nextUrl.pathname.startsWith("/dashboard") || req.nextUrl.pathname.startsWith("/login") || req.nextUrl.pathname.startsWith("/register")) {
// console.log("matches")
// const token = await getToken({ req });
// const isAuth = !!token;
// const isAuthPage = req.nextUrl.pathname.startsWith("/login") || req.nextUrl.pathname.startsWith("/register");
// if (isAuthPage) {
// if (isAuth) {
// return NextResponse.redirect(new URL("/dashboard", req.url));
// }
// return null;
// }
// if (!isAuth) {
// let from = req.nextUrl.pathname;
// if (req.nextUrl.search) {
// from += req.nextUrl.search;
// }
// return NextResponse.redirect(new URL(`/login?callbackUrl=${encodeURIComponent(from)}`, req.url));
// }
// }
},
{
callbacks: {
async authorized() {
return true;
}
}
}
);
i have tried to comment out the middleware (which had no effect on the error)
and i have also made the matcher only target one page, which worked, however i need it to target all pages so that the middleware function works

How to create a function that returns new session format with extra key value pair

I am using NextJS with NextAuth with google and email providers. Unfortunately, the session returns only few fields that does not include userId of the user from the database.
I created however a function that I intend to use with every getServerSideProps request. The function returns the following:
{
user: {
name: 'daniel sas',
email: 'emailofuser#gmail.com',
image: 'https://lh3.gooleusercontent.com/a/AEdFTp6r44ZwqcfJORNnuYtbVv_LYbab-wv5Uyxk=s96-c',
userId: 'clbcpc0hi0002sb1wsiea3q5d'
},
expires: '2022-12-17T20:18:52.580Z'
}
The problem is I am getting an error that does not allow me to pass the props in the page:
Error: Your `getServerSideProps` function did not return an object. Did you forget to add a `return`?
In the function I get the user by the email, and attach the userId.
import { getSession } from "next-auth/react";
import prisma from './prisma'
// This function get the email and returns a new session object that includes
// the userId
export const requireAuthentication = async context => {
const session = await getSession(context);
const errorOrUserNotFound = () => {
return {
redirect: {
destination: '/signup',
permanent: false
}
}
}
// If there is no user or there is an error ret to signup page
if (!session) {
errorOrUserNotFound();
}
// If the user is not found return same redirect to signup
else {
try {
const user = await prisma.user.findUnique({where: { email: session.user.email }});
if (!user) return errorOrUserNotFound();
// Must return a new session here that contains the userId...
else {
const newSession = {
user: {
...session.user,
userId: user.id
},
expires: session.expires
};
console.log(newSession);
return {
props: {
session: newSession
}
}
}
}
catch (error) {
if (error) {
console.log(error);
}
}
}
}
The react component looks like this. In the getServerSideProps i return the await function. The problem is that when I log the prop in the serverside, I get the following:
{
props: { session: { user: [Object], expires: '2022-12-17T20:18:52.580Z' } }
}
However, if i log the props in the clientside, I get an empty object...
//Clientside compoen
import { getSession } from "next-auth/react"
import { Fragment, useState } from "react";
import { requireAuthentication } from "../../lib/requireAuthentication";
import CreateListModal from "./CreateListModal";
const DashboardPage = props => {
const [loading, setloading] = useState(false);
console.log(props);
return (
<section className="border-4 border-orange-800 max-w-5xl mx-auto">
<CreateListModal userId={props.userId} loading={loading} setloading={setloading} />
</section>
)
}
export const getServerSideProps = async context => {
const session = await getSession(context);
const reqAuth = await requireAuthentication(context);
console.log(reqAuth);
return reqAuth
}
export default DashboardPage;

How do you do server side rendering with nextjs [id].js in headless wordpress? fetch single page using graphql from Wordpress. like service/[id].js

I have a nextjs project that is using apollo graphql to fetch data from the backend. I am trying to render my page using server side rendering. But I am currently using graphql apollo hooks to fetch my data from the backend, and the react hooks prevents me from calling my backend inside of the getServerSideProps.
Create and fetch single page using graphql from Wordpress with clean URLs like services/[id].js
N.B: Warning Show ( Error: Response not successful: Received status code 500)
import {
gql,
ApolloClient,
InMemoryCache
} from "#apollo/client";
export const client = new ApolloClient({
uri: 'https://.........../graphql',
cache: new InMemoryCache()
});
const serviceDetail = (serviceOutput) => {
return (
<div>
{serviceOutput.serviceTitle}
{serviceOutput.serviceContent}
</div>
)
}
export const getServerSideProps = async (context) => {
const result = await client.query({
query: gql`
query serData($id: id!) {
HomePage: pageBy(uri: "https://......./home/") {
aboutSection {
serviceSec(id: $id) {
id
serviceTitle
serviceContent
serviceImage {
sourceUrl
}
}
}
}
}
`,
variables: {
id: context.params.id
}
})
return {
props: {
serviceOutput: result.data.HomePage.aboutSection.serviceSec;
},
};
}
export default serviceDetail;
i am not an expert, but as far i have used. you cannot use Apollo together with next js fetching method(ssg,ssr,isr).
Apollo runs queries on client side, and can be used with useQuery and useLazyQuery. while next js fetching is completely different.
I will demonstrate 2 ways here.
-- Using Apollo --
const FETCH_ALL = gql`
query MyQuery($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
title
}
}
}
}
`;
export default function LoadMoreList() {
const { data } = useQuery(FETCH_ALL, {
variables: { first: 5, after: null },
notifyOnNetworkStatusChange: true,
});
return (
<>
<div>
{postdata.map((node, index) => {
{
return (
<div key={index}>
<h1>{node?.node?.title}</h1>
</div>
);
}
})}
</div>
</>
)}
=== using fetch and getStaticProps ==
--File1 (this is a fetch function, to which you pass your queries and variables)
async function fetchAPI(query, { variables } = {}) {
const headers = { "Content-Type": "application/json" };
const res = await fetch(process.env.WP_API, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) {
console.log(json.errors);
throw new Error("Failed to fetch API");
}
return json.data;
}
export default fetchAPI;
-- File2 (this is a file that contains your query)
import fetchAPI from "./fetching";
export async function homeheadposts() {
const data = await fetchAPI(
`
query homeheadposts {
posts(first: 7) {
edges {
node {
id
slug
title
featuredImage {
node {
sourceUrl
}
}
excerpt(format: RAW)
}
}
}
}
`
);
return data?.posts;
}
-- File3 (place this function , where you wanna call and use the data, )
export async function getStaticProps() {
const latestPosts = await homeheadposts();
return {
props: { latestPosts },
};
}
export default function CallingData({ latestPosts }) {
console.log(latestPosts);
return <h1>hello</h1>;
}

nextJS SSR useRouter() does not work when refresh page

I am using nextJS SSR in my project. Now when I try to use the following code to get page parameters then it shows undefined.
function About() {
const router = useRouter();
const { plan_id } = router.query;
console.log(plan_id)
}
export default About;
It works when the page is routed from some other page (without page reload with "next/link") but it does not work when I refresh the page. Can someone please help?
I found the answer self. Actually when you refresh the page then the router does not get initialized instantly. So you can add that under UseEffect hook as following and you will be able to get the parameters
function About() {
const [param1, setParam1]=useState("");
const router = useRouter();
useEffect(() => {
if (router && router.query) {
console.log(router.query);
setParam1(router.query.param1);
}
}, [router]);
}
When this router parameter will change then it will call the "UseEffect" which can be used to retrieve the values.
function About({plan_id}) {
console.log(plan_id)
}
// this function only runs on the server by Next.js
export const getServerSideProps = async ({params}) => {
const plan_id = params.plan_id;
return {
props: { plan_id }
}
}
export default About;
You can find more intel in the docs.
I fix this problem with this method.
First add getServerSideProps to your page
//MyPage.js
export async function getServerSideProps({req, query}) {
return {
props: {
initQuery: query
}
}
}
Then created useQuery function like this
//useQuery.js
export let firstQuery = {}
export default function useQuery({slugKey = 'slug', initial = {}} = {}) {
const {query = (initial || firstQuery)} = useRouter()
useEffect(() => {
if (_.isEmpty(initial) || !_.isObject(initial))
return
firstQuery = initial
}, [initial])
return useMemo(() => {
if (!_.isEmpty(query)) {
return query
}
try {
const qs = window.location.search.split('+').join(' ');
const href = window.location.href
const slug = href.substring(href.lastIndexOf('/') + 1).replace(/\?.*/gi, '')
let params = {},
tokens,
re = /[?&]?([^=]+)=([^&]*)/g;
if (slug)
params[slugKey] = slug
while (tokens = re.exec(qs)) {
params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
}
return params
} catch {
}
}, [query])
}
And always use useQuery for receive query params
//MyPage.js
export default function MyPage({initQuery}) {
const query = useQuery({initial: initQuery})
return(
<div>
{query.myParam}
</div>
)
}
And in components like this
//MyComponent.js
export default function MyComponent() {
const query = useQuery()
return(
<div>
{query.myParam}
</div>
)
}
For those still having issues with this. Here is a solution that worked for me
function About() {
const [param1, setParam1]=useState("");
const router = useRouter();
const { param1 } = router.query()
useEffect(() => {
if (!param1) {
return;
}
// use param1
}, [param1]);
}
You can find the solution here

How to #connect to async loaded data?

I'm writing an app with React, Redux, react-redux, react-router, react-router-redux and redux-async-connect. The special part of the app is that all client-server communication is done over websockets.
My users can read/write several walls, that I store is a walls store with the following reducer and basic helper functions:
const LOAD = 'ws/wall/LOAD';
const LOAD_SUCCESS = 'estisia/wall/LOAD_SUCCESS';
const LOAD_FAIL = 'estisia/wall/LOAD_FAIL';
const ADD_MESSAGE = 'estisia/wall/ADD_MESSAGE';
const initialWallState = {
loaded: false,
messages: []
};
const initialState = {
walls: {}
};
function wall(state = initialWallState, action = {}) {
switch (action.type) {
... // omitted for brevity
default:
return state;
}
}
export default function walls(state = initialState, action = {}) {
if (typeof action.data === 'undefined' || typeof action.data.wallId === 'undefined') return state;
const newState = {
...state.walls,
[action.data.wallId]: wall(state.walls[action.data.wallId], action)
};
return {walls: newState};
}
export function isLoaded(wallId, globalState) {
return globalState.walls.wallId && globalState.walls.wallId.loaded;
}
export function load(wallId) {
return {
type: LOAD,
send: ['wall/messages/page', [wallId, 1]]
};
}
and I have a Wall container where the appropriate wallId is passed in by react-router, but can't figure out how to make the #connect call pass only the relevant wall data to the container. What should I do instead of the #connect meta-code below to get it working?
#asyncConnect([{
deferred: true,
promise: ({store: {dispatch, getState}, params}) => {
if (!isLoaded(params.wallId, getState())) {
return dispatch(loadWall(params.wallId));
}
}
}])
#connect(
state => ({
messages: state.wall[this.params.wallId].messages,
loading: state.wall[this.params.wallId].loading
}))
export default class Wall extends Component {
static propTypes = {
messages: PropTypes.array
}
... // omitted for brevity
}
How can I achieve redux-async-connect to help me out with the above case?

Resources