Cross-domain is a common problem with Next.js, and there are two main types of problems:
- How do I call a cross-domain interface?
- How to implement a cross-domain interface?
In this post, we’ll start with the basics of cross-domaining, and then we’ll explain the solutions to these two main types of problems with Next.js, to help you solve cross-domaining problems in a systematic way! Bookmark and like this post to keep it in your memory!
PS: systematic learning Next.js, welcome to the booklet “Next.js Development Guide”. Next.js development guide. Basic chapter, combat chapter, source code chapter, interview chapter four chapters to take you to systematically master Next.js!
1. Basics
Let’s review the basics of cross-domain and CORS.
1.1 Cross-domain
Cross-domain is browser behavior. For security reasons, browsers limit cross-origin HTTP requests initiated within scripts. both XMLHttpRequest and the Fetch API follow the same-origin policy.
By same-origin policy, we mean that the protocol/hostname/port of two URLs are the same. For example, https://www.taobao.com
, its protocol is https
, its hostname is www.taobao.com
, and its port is 443
. Only when the protocol, hostname and port are exactly the same, it is considered as same-origin.
This means that Web applications using these APIs can only request HTTP resources from the same domain that loaded the application, unless the response message contains the correct CORS response headers.
For example, if we are on the Nuggets homepage, open the browser console and request the Taobao interface (e.g., https://www.taobao.com
, we will get a CORS error message:
1.2 CORS
To enable cross-domain requests, there is CORS (Cross-Origin Resource Sharing), which is a mechanism based on the HTTP header that allows browsers to allow these sources to access load their own resources by allowing the server to indicate other sources (domains, protocols, or ports) in addition to its own.
Simply put, CORS is also a browser-implemented mechanism that allows cross-domain requests for HTTP resources.
The browser will determine whether the request is cross-domain or not, if it is cross-domain, the request header will take the Origin attribute, and after the data is returned, it will check whether the Access-Control-Allow-Origin in the response header matches or not, if it does not match, it will report a CORS error.
For example, if we request https://api..cn/content_api/v1/content/article_rank?category_id=1&type=hot
from the Nuggets homepage https://.cn/
and the console, it is a cross-domain request because the hostnames don’t match:
But since CORS is set up, the request works fine. Let’s look at the request header and response header:
The Origin in the request header is https://.cn
, and the Access-Control-Allow-Origin in the response header is https://.cn
, which matches, so you can get the data across domains normally.
Of course, this is for simple requests; for complex requests, such as POST with customized headers, the browser sends an OPTIONS request for preflighting.
Simply put, the browser also sends an OPTIONS-type request to check if a cross-domain request is possible before sending a formal request. If the request passes, then the formal request is sent.
Let’s say we send a POST request with a customized request header in the console:
fetch("https://api..cn/content_api/v1/content/article_rank?category_id=1&type=hot", { method: "POST", headers: {
"x-custom": "yayu"
}});
The effect is as follows:
In this case, the browser will first send an OPTIONS request, which will be carried in the request header:
Access-Control-Request-Headers: x-custom,x-secsdk-csrf-token
Access-Control-Request-Method: POST
Origin indicates that the request source is https://.cn
and the header Access-Control-Request-Method informs the server that the actual request will use the POST method. The Access-Control-Request-Headers header informs the server that the actual request will carry two custom request header fields: x-custom and x-secsdk-csrf-token, which the server uses to determine if the actual request is allowed.
The server returns the response header:
Access-Control-Allow-Headers: x-custom,x-secsdk-csrf-token
Access-Control-Allow-Methods: POST
It means: OK, all accepted! Then the browser sends the actual request.
That’s it for the basics of cross-domain and CORS, so how do we Next.js handle cross-domain?
Let’s start with how to call the cross-domain interface in Next.js.
2. call a cross-domain interface?
Let’s start by modeling a cross-domain problem. Using the official scaffolding, create a Next.js project:
npx create-next-app@latest
Modify app/page.js
with the following code:
'use client'
import { useEffect, useState } from "react";
export default function Home() {
const [html, setHTML] = useState([]);
useEffect(() => {
const fetchArticleList = async () => {
const response = await fetch("https://www..com");
const data = await response.text()
setHTML(data)
}
fetchArticleList()
}, [])
return (
<div>{html}</div>
);
}
Run npm run dev
to enable development mode. The local address is http://localhost:3000/
and the request interface, https://www..com
, gives a CORS error because both the protocol and the hostname do not match:
So what can be done to avoid CORS errors?
2.1 Using server-side components
The first solution is to switch to using server-side components.
Cross-domain errors are browser behavior. Changing to a server-side component is essentially changing to a Node back-end call, so naturally there will be no cross-domain issues.
Modify app/page.js
with the following code:
export default async function Home() {
const response = await fetch("https://www..cn");
const html = await response.text()
return (
<div>{html}</div>
);
}
The browser effect is as follows:
2.2 Forwarding using back-end interfaces
If we can’t change to using server-side components, we can also use Next.js to customize an interface that the front-end calls instead.
Create a new app/api//route.js
with the following code:
export async function GET() {
const res = await fetch('https://www..cn')
const data = await res.text()
return Response.json({ data })
}
In this way we have implemented a GET interface to http://localhost:3000/api/
.
Modify app/page.js
, still using the client component, to call this interface instead. The code is as follows:
'use client'
import { useEffect, useState } from "react";
export default function Home() {
const [html, setHTML] = useState([]);
useEffect(() => {
const fetchArticleList = async () => {
const response = await fetch("http://localhost:3000/api/");
const data = await response.json()
setHTML(data.data)
}
fetchArticleList()
}, [])
return (
<div>{html}</div>
);
}
The browser effect is as follows:
When using a server-side component, the request is not viewed in the browser because it is called from the back-end instead. With this method, on the other hand, the request is still essentially sent on the browser side, so the request can be viewed in the browser.
2.3. Using the rewrites configuration item
Next.js actually provides a rewrites configuration for rewriting requests. This is a common way to address cross-domain issues.
The rewrite will map the incoming request path to some other destination path. You can think of it as a proxy, and it will mask the destination path so that it doesn’t appear that the user has changed their location on the site.
Modify next.config.mjs
with the following code:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/',
destination: 'https://.cn',
}
]
}
};
export default nextConfig;
By configuring the rewrites
configuration item in next.config.js
, the proxy requests https://.cn
when /api/
is requested.
Modify app/page.js
with the following code:
'use client'
import { useEffect, useState } from "react";
export default function Home() {
const [html, setHTML] = useState([]);
useEffect(() => {
const fetchArticleList = async () => {
const response = await fetch("http://localhost:3000/api/");
const data = await response.text()
setHTML(data)
}
fetchArticleList()
}, [])
return (
<div>{html}</div>
);
}
The browser effect is as follows:
2.4 Use of middleware
Not only can next.config.js
be configured to rewrite, you can also implement rewriting in middleware.
Create a new middleware.js
in the root directory with the following code:
import { NextResponse } from 'next/server';
export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.rewrite(new URL('https://.cn'))
}
}
export const config = {
matcher: '/api/:path*',
}
Modify app/page.js
with the following code:
'use client'
import { useEffect, useState } from "react";
export default function Home() {
const [html, setHTML] = useState([]);
useEffect(() => {
const fetchArticleList = async () => {
const response = await fetch("http://localhost:3000/api/");
const data = await response.text()
setHTML(data)
}
fetchArticleList()
}, [])
return (
<div>{html}</div>
);
}
The browser effect remains unchanged at this time:
3. Implement a cross-domain interface?
What if we want to implement an interface that allows access from other sources?
The key to implementation is to add the Access-Control-Allow-Origin response header, so where do I add this response header?
There are actually many options to choose from:
3.1 Routing procedures
If there is only one or a small number of interfaces that need to implement cross-domain, then it can be written directly in the corresponding route handler.
Create a new app/api/blog/route.js
with the following code:
import { NextResponse } from 'next/server'
export async function GET() {
const data = { success: true, data: { name: "yayu"}}
return NextResponse.json(data)
}
This way we have implemented an interface which is available at http://localhost:3000/api/blog
.
We open the Nuggets homepage and in the browser console, request the address:
Obviously a CORS error will occur.
Modify app/api/blog/route.js
with the following code:
import { NextResponse } from 'next/server'
export async function GET() {
const data = { success: true, data: { name: "yayu"}}
return NextResponse.json(data, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
At this point, the interface has been set up to support cross-domain requests with CORS. We make another request from the Nuggets homepage, and it responds normally:
3.2 Middleware setup
In the case of multiple interfaces, this can be configured in middleware or next.config.js
.
Let’s start with using middleware, which is sort of the most common solution to cross-domain problems.
Change the app/api/blog/route.js
code back to the previous code that had the cross-domain issue:
import { NextResponse } from 'next/server'
export async function GET() {
const data = { success: true, data: { name: "yayu"}}
return NextResponse.json(data)
}
Modify middleware.js
with the following code:
import { NextResponse } from 'next/server'
const allowedOrigins = ['https://.cn']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request) {
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
The middleware provides a global control mechanism for incoming requests. We have unified the CORS determination and processing for /api/xxx
in middleware, and at this point we can request the address in the Nuggets home console, and it works fine:
Let’s try the preflight request again, with the browser console running:
fetch("http://localhost:3000/api/blog", { method: "POST", headers: {
"Content-Type": "application/xml"
}});
Because of the customized request header value, an OPTIONS request for preflight is triggered. It is also possible to make a normal request:
Note: Note the Access-Control-Allow-Headers in our code, if we carry other request headers, it will also result in a CORS error because of the mismatched request headers.
3.3. Using the headers configuration item
In addition to setting this up in middleware, you can also use the headers configuration item in next.config.js.
Modify next.config.mjs
with the following code:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
}
};
export default nextConfig;
This is kind of a simple implementation. If you need more flexibility, it’s still middleware. At this point it is also possible to make normal requests:
3.4. Vercel configuration items
If you use Vercel, you can also configure it in vercel.json:
{
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Credentials", "value": "true" },
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" },
{ "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" }
]
}
]
}