Description
I am running into this issue, where the Access-Control-Allow-Origin
header is missing from the /_vercel/insights/view
endpoint when reverse proxying (so it works when I am on the request URL directly but not when there's a referrer url. I have added the headers to the vercel.json
, the next.config.js
, the middleware that handles it, and even tried on the proxy's side via Cloudflare. Hoping to get some help in solving this, and share as much context as I can
The vercel.json
file:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{
"source": "(.*sitemap.xml[/]?.*)",
"destination": "/api/sitemaps"
}
],
"headers": [
{
"source": "/(.*)",
"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"
}
]
}
],
"github": {
"silent": true
}
}
The next.config.js
const path = require('path')
const { withSentryConfig } = require('@sentry/nextjs')
const { version } = require('./package.json')
/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: process.env.ASSET_PREFIX ?? '',
compiler: {
styledComponents: true,
},
reactStrictMode: false,
images: {
domains: ['asset-url', 'another-asset-url'],
loader: 'custom',
loaderFile: './src/imageLoader.js',
formats: ['image/avif', 'image/webp'],
},
webpack: (config, options) => {
const { dev, isServer } = options
config.resolve.alias['styled-components'] = path.resolve(
'./node_modules/styled-components'
)
return config
},
logging: {
fetches: {
fullUrl: true,
},
level: 'verbose',
},
modularizeImports: {
lodash: {
transform: 'lodash/{{member}}',
},
},
experimental: {
forceSwcTransforms: true,
instrumentationHook: true,
},
env: {
VERSION: version,
},
async headers() {
return [
{
source: '/(.*)',
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',
},
],
},
]
},
}
module.exports = withSentryConfig(
// withBundleAnalyzer(nextConfig),
nextConfig,
{
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
// Suppresses source map uploading logs during build
silent: true,
org: 'my-org',
project: 'my-project',
},
{
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: false,
// // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. (increases server load)
// // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// // side errors will fail.
// tunnelRoute: '/monitoring',
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
environment: process.env.VERCEL_ENV,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
)
The middleware
import { NextResponse, NextRequest } from 'next/server'
import { addQueryStringParams, join, getFullIDUrl } from './api/ssr/utils'
import { OrderAttribution } from 'shared'
import { notFound } from 'next/navigation'
import { logMiddlewareError } from './utils/sentry'
const PROXY_TYPE_ID = 'myorg-type-id'
const PROXY_TYPE_ID_OLD = 'myorg-id'
const PROXY_TYPE_PATH = 'myorg-exampleB-path'
const PROTOCOL = process.env.NEXT_PUBLIC_ENV === 'local' ? 'http' : 'https'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
if (request.method === 'HEAD') {
console.log('HEAD request, returning 200 request', request)
console.log('HEAD request, returning 200 response', NextResponse.json({}, { status: 200 }))
return NextResponse.json({}, { status: 200 })
}
// NOTE: here I have tried adding these headers as both the request headers and the response headers to no avail.
request.headers.set('Access-Control-Allow-Origin', '*')
request.headers.set('Access-Control-Allow-Credentials', 'true')
request.headers.set('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT')
request.headers.set('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version')
if (request.method !== 'GET') {
return response
}
const requestUrl = new URL(request.nextUrl)
const nextUrl = request.nextUrl.clone()
try {
const requestHeaders = new Headers(request.headers)
const id =
requestHeaders.get(PROXY_TYPE_ID) ??
requestHeaders.get(PROXY_TYPE_ID_OLD)
let storePath = requestHeaders.get(PROXY_TYPE_PATH)
typePath = typePath?.startsWith('/') ? typePath : `/${typePath}`
const nextJsUrl = request.nextUrl.toString()
requestHeaders.set('x-pathname', request.nextUrl.pathname)
requestHeaders.set('x-url', nextJsUrl)
const nextUrlSearchParams = nextUrl.searchParams
addRequestHeaders({
nextUrl,
searchParams: nextUrlSearchParams,
requestHeaders,
})
// ---- proxying ----
//if we have an id and an external path, we are being proxied to.
//so we need to rewrite the url to the correct path.
//the incoming url will be something like '/place/type/things'.
//we need to rewrite it to '/:id/stuff/things':
// -- the 'myorg-type-id' is set by the customer's proxy
// to tell us which store we are on, used to replace ${id} below
// -- the 'myorg-type-path' is set by the customer's proxy
// to tell us which part of the url our stuff should come after
//ex. '/place/type'
//we replace that with the id and '/endpoint' which matches the folder structure for NextJS (src/app/[id]/endopint/thing.tsx).
if (id && typePath) {
console.log(
`----- PROXYING id:${id} typePath:${typePath} -----`
)
if (nextUrl.pathname === '/robots.txt') {
return NextResponse.rewrite(new URL('/robots.txt', nextUrl))
}
const newUrl = new URL(
requestUrl.pathname.replace(path, ''),
`${PROTOCOL}://${requestUrl.hostname}:${requestUrl.port}`
)
if (nextUrl.pathname.endsWith('/robots.txt')) {
return NextResponse.rewrite(new URL('/robots.txt', newUrl))
}
const dir = newUrl.pathname.startsWith('/exampleA') ? '' : 'exampleB'
const rewritePathname = addQueryStringParams(
join(`/${id}/${dir}`, newUrl.pathname),
Object.fromEntries(nextUrlSearchParams.entries()) ?? {}
)
const rewriteUrl = new URL(rewritePathname, newUrl.toString())
return NextResponse.rewrite(rewriteUrl, {
headers: requestHeaders,
})
}
// console.log('----- NO PROXYING -----')
if (nextUrl.pathname === '/our-endpoint' && nextUrlSearchParams.get('id')) {
const newUrl = new URL(request.url)
const rewritePathname = addQueryStringParams(
join(`/${nextUrlSearchParams.get('id')}`, nextUrl.pathname),
Object.fromEntries(newUrl.searchParams.entries()) ?? {}
)
return NextResponse.rewrite(new URL(rewritePathname, newUrl.toString()), {
headers: requestHeaders,
})
}
//ORG Iframe Connect Mode
//ORG consumer apps should point to /{id}/org-connect?orgUid=12345&anAuthToken=4567
if (nextUrl.pathname.match(/\/[0-9A-Z&\-%]*\/org-connect/i)) {
if (
!nextUrlSearchParams.get('orgUid') ||
!nextUrlSearchParams.get('anAuthToken')
) {
return NextResponse.rewrite(new URL('/not-found', nextUrl.toString()), {
headers: requestHeaders,
})
}
requestHeaders.set('an-attribute, 'true')
}
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
} catch (error) {
logMiddlewareError(error, {
url: requestUrl.pathname,
})
return notFound()
}
}
function addRequestHeaders({
nextUrl,
searchParams,
requestHeaders,
}: {
nextUrl: URL
searchParams: URLSearchParams
requestHeaders: Headers
}) {
...
...
...
requestHeaders.set(
'example-myorg',
searchParams.get('example') ?? ''
)
...
}
export const config = {
matcher: [
'/((?!api|_vercel|_next/static|/monitoring|_next/image|favicon.ico|robots.txt|ads.txt|sitemap.xml|manifest.json|android-chrome-*.png|apple-touch-icon.png|browserconfig.xml|mstile-150x150.png|safari-pinned-tab.svg|site.webmanifest|favicon-.*.png).*)',
],
}
How we set analytics:
import 'server-only'
import { headers as getHeaders } from 'next/headers'
import Script from 'next/script'
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'
......
export default async function Layout({
children,
params,
}: {
children: React.ReactNode
params: { id: string }
}) {
const headers = getHeaders()
const pathname = getNextJsPathname(headers)
...
const id = params.id
if (!pathname) throw new Error('missing pathname')
const routeName =
variant === 'exampleB' && id
? getexampleBRouteName({
id,
headers,
})
: getCurrentExampleRouteName(pathname)
const vercelAnalyticsSrc = process.env.ASSET_PREFIX
? join(process.env.ASSET_PREFIX, '_vercel/insights/script.js')
: '/_vercel/insights/script.js'
const vercelAnalyticsEndpoint = process.env.ASSET_PREFIX
? join(process.env.ASSET_PREFIX, '_vercel/insights')
: '/_vercel/insights'
const vercelSpeedInsightsSrc = process.env.ASSET_PREFIX
? join(process.env.ASSET_PREFIX, '_vercel/speed-insights/script.js')
: '/_vercel/speed-insights/script.js'
const vercelSpeedInsightsEndpoint = process.env.ASSET_PREFIX
? join(process.env.ASSET_PREFIX, '_vercel/speed-insights/vitals')
: '_vercel/speed-insights/vitals'
try {
const appConfig = await getAppConfigById(id)
if (!appConfig) {
return notFound()
}
let googleFontsEnabled = false
let googleFontString = ''
let customFontString = ''
let cssVars = ''
...
const queryClient = getSSRQueryClient()
...
...
return (
<html
lang="en"
title={title}
>
<head>
<style
id="myorg-exampleB-styles"
dangerouslySetInnerHTML={{
__html: `
:root {
${cssVars}
}
...{style stuff}
/>
{googleFontString && (
<link
href={`https://fonts.googleapis.com/css?family=${googleFontString}&display=swap`}
rel="stylesheet"
/>
)}
<ErrorBoundary error="custom-head-code-error">
{appConfig.place.exampleHeadCode
? htmlReactParser(appConfig.place.exampleBHeadCode)
: null}
</ErrorBoundary>
...
...
</head>
<body className={bodyClassName}>
...
...
...
{process.env.ENV !== 'local' && (
<>
<SpeedInsights
scriptSrc={vercelSpeedInsightsSrc}
endpoint={vercelSpeedInsightsEndpoint}
/>
<Analytics
scriptSrc={vercelAnalyticsSrc}
endpoint={vercelAnalyticsEndpoint}
/>
</>
)}
...
...
...
</body>
</html>
)
} catch (error) {
....
}
export const runtime = 'edge'
...
Then in Cloudflare, I tried from the org doing to proxy to set the headers following their docs, with no such luck.
In the browser, we see a CORs error with a missing header, and I do see it's missing but can't seem to add it with any of the above
I suspect there's some issue either with how we have Vercel Analytics implemented, or NextJS is stripping the header at some point, but have hit a bit of a wall and could use new eyes. Thanks in advance for any help!