mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
refactor: move /incident-banner to app router (#43930)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Refactor ## What is the current behavior? The `/incident-banner` endpoint is implemented using the Pages Router. ## What is the new behavior? The `/incident-banner` endpoint is moved to the App Router, enabling caching of the upstream fetch. This does not turn on the querying from the frontend yet, making that a separate PR so we can revert easily if needed. ## Additional context
This commit is contained in:
1 parent
df7e98bae6
commit
fe0da16820
2 files changed
+88
-38
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import { InternalServerError } from '@/lib/api/apiHelpers'
|
||||
import { getBannerIncidents } from '@/lib/api/incident-banner'
|
||||
|
||||
/**
|
||||
* Cache on CDN for 5 minutes
|
||||
* Allow serving stale content for 1 minute while revalidating
|
||||
*/
|
||||
const CACHE_CONTROL_SETTINGS = 'public, s-maxage=300, stale-while-revalidate=60'
|
||||
|
||||
export async function OPTIONS() {
|
||||
if (!IS_PLATFORM) return new Response(null, { status: 404 })
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
Allow: 'GET, HEAD, OPTIONS',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function HEAD() {
|
||||
if (!IS_PLATFORM) return new Response(null, { status: 404 })
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: { 'Cache-Control': CACHE_CONTROL_SETTINGS },
|
||||
})
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
if (!IS_PLATFORM) return new Response(null, { status: 404 })
|
||||
|
||||
try {
|
||||
const incidents = await getBannerIncidents()
|
||||
return NextResponse.json(
|
||||
{ incidents },
|
||||
{ headers: { 'Cache-Control': CACHE_CONTROL_SETTINGS } }
|
||||
)
|
||||
} catch (error) {
|
||||
let errorCode = 500
|
||||
const headers = new Headers()
|
||||
|
||||
if (error instanceof InternalServerError) {
|
||||
if (typeof error.details?.status === 'number') errorCode = error.details.status
|
||||
if (errorCode === 420) errorCode = 429
|
||||
if (errorCode === 429 && typeof error.details?.retryAfter === 'string') {
|
||||
headers.set('Retry-After', error.details.retryAfter)
|
||||
}
|
||||
console.error('Failed to fetch incident.io incidents: %O', {
|
||||
message: error.message,
|
||||
details: error.details,
|
||||
})
|
||||
} else {
|
||||
console.error('Unexpected error fetching incident.io incidents: %O', error)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: { message: 'Internal server error' } },
|
||||
{ status: errorCode, headers }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IS_PROD } from 'common'
|
||||
import { createHash } from 'crypto'
|
||||
import { IS_PLATFORM, IS_PROD } from 'common'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { z } from 'zod'
|
||||
import z from 'zod'
|
||||
|
||||
import { InternalServerError } from 'lib/api/apiHelpers'
|
||||
|
||||
const INCIDENT_IO_BASE_URL = 'https://api.incident.io/v2'
|
||||
|
||||
@@ -15,13 +16,6 @@ const SENTINEL_VALUE_FORCE_BANNER = '100'
|
||||
|
||||
const FALLBACK_METADATA = { affected_regions: null, affects_project_creation: false }
|
||||
|
||||
/**
|
||||
* Cache on browser for 5 minutes
|
||||
* Cache on CDN for 5 minutes
|
||||
* Allow serving stale content for 1 minute while revalidating
|
||||
*/
|
||||
const CACHE_CONTROL_SETTINGS = 'public, max-age=300, s-maxage=300, stale-while-revalidate=60'
|
||||
|
||||
const MetadataSchema = z.object({
|
||||
affected_regions: z.union([z.array(z.string()), z.null()]),
|
||||
affects_project_creation: z.boolean(),
|
||||
@@ -51,9 +45,9 @@ interface IncidentIoListResponse {
|
||||
pagination_meta?: { after?: string }
|
||||
}
|
||||
|
||||
type ShowBannerValue = true | 'force'
|
||||
export type ShowBannerValue = true | 'force'
|
||||
|
||||
interface BannerIncident {
|
||||
export interface BannerIncident {
|
||||
id: string
|
||||
show_banner: ShowBannerValue
|
||||
metadata: z.infer<typeof MetadataSchema> & { force: boolean }
|
||||
@@ -83,11 +77,18 @@ async function fetchAllIncidents(apiKey: string, mode: string): Promise<Array<In
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 180 },
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const cause = await response.text()
|
||||
throw new Error(`incident.io API responded with status ${response.status}`, { cause })
|
||||
const retryAfter = response.headers.get('Retry-After') ?? undefined
|
||||
const body = await response.text()
|
||||
throw new InternalServerError(`incident.io API responded with ${response.status}`, {
|
||||
status: response.status,
|
||||
body,
|
||||
...(retryAfter !== undefined && { retryAfter }),
|
||||
})
|
||||
}
|
||||
|
||||
const data: IncidentIoListResponse = await response.json()
|
||||
@@ -98,31 +99,20 @@ async function fetchAllIncidents(apiKey: string, mode: string): Promise<Array<In
|
||||
return incidents
|
||||
}
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!IS_PLATFORM) {
|
||||
return res.status(404).end()
|
||||
}
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
res.setHeader('Allow', ['GET'])
|
||||
return res.status(405).json({ error: { message: `Method ${req.method} Not Allowed` } })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches active banner incidents from the incident.io API.
|
||||
*
|
||||
* @returns Array of banner incidents
|
||||
* @throws Error if INCIDENT_IO_API_KEY is not set or the API returns an error
|
||||
*/
|
||||
export async function getBannerIncidents(): Promise<Array<BannerIncident>> {
|
||||
const apiKey = process.env.INCIDENT_IO_API_KEY
|
||||
if (!apiKey) {
|
||||
console.error('INCIDENT_IO_API_KEY is not set')
|
||||
return res.status(500).json({ error: { message: 'Internal server error' } })
|
||||
throw new Error('INCIDENT_IO_API_KEY is not set')
|
||||
}
|
||||
|
||||
const incidentMode = IS_PROD ? 'standard' : 'test'
|
||||
|
||||
let allIncidents: Array<Incident>
|
||||
try {
|
||||
allIncidents = await fetchAllIncidents(apiKey, incidentMode)
|
||||
} catch (error) {
|
||||
console.error('Error fetching incidents from incident.io: %O', error)
|
||||
return res.status(502).json({ error: { message: 'Internal server error' } })
|
||||
}
|
||||
const allIncidents = await fetchAllIncidents(apiKey, incidentMode)
|
||||
|
||||
const bannerIncidents: Array<BannerIncident> = []
|
||||
|
||||
@@ -152,8 +142,5 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
})
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', CACHE_CONTROL_SETTINGS)
|
||||
return res.status(200).json({ incidents: bannerIncidents })
|
||||
return bannerIncidents
|
||||
}
|
||||
|
||||
export default handler
|
||||
Reference in new issue
Block a user