Skip to content

Commit 6e5b92b

Browse files
committed
fix(runtime): parse queries from event paths
1 parent 7bc5188 commit 6e5b92b

6 files changed

Lines changed: 51 additions & 6 deletions

File tree

src/runtime/server/og-image/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type BrowserRenderer from './browser/renderer'
88
import type SatoriRenderer from './satori/renderer'
99
import type TakumiRenderer from './takumi/renderer'
1010
import { defu } from 'defu'
11-
import { createError, getQuery } from 'h3'
11+
import { createError } from 'h3'
1212
import { useNitroApp } from 'nitropack/runtime'
1313
import { parseURL, withoutLeadingSlash, withoutTrailingSlash, withQuery } from 'ufo'
1414
import { normalizeKey } from 'unstorage'
@@ -21,6 +21,7 @@ import { hashKey } from '../../shared/hash'
2121
import { autoEjectCommunityTemplate } from '../util/auto-eject'
2222
import { createNitroRouteRuleMatcher } from '../util/kit'
2323
import { normaliseOptions } from '../util/options'
24+
import { getEventQuery } from '../util/query'
2425
import { createTimings, TIMING_CTX_KEY } from '../util/timings'
2526
import { withTimeout } from '../util/withTimeout'
2627
import { useOgImageRuntimeConfig } from '../utils'
@@ -140,7 +141,7 @@ export async function resolveContext(e: H3Event): Promise<H3Error | OgImageRende
140141
// In production they're ignored since all options are encoded in the URL path.
141142
let queryParams: Record<string, any> = {}
142143
if (import.meta.dev || import.meta.prerender) {
143-
const query = getQuery(e)
144+
const query = getEventQuery(e)
144145
for (const k in query) {
145146
const v = String(query[k])
146147
if (!v)

src/runtime/server/routes/resolve.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { H3Event } from 'h3'
2-
import { createError, defineEventHandler, getQuery, getRequestHost, sendRedirect } from 'h3'
2+
import { createError, defineEventHandler, getRequestHost, sendRedirect } from 'h3'
33
import { parseURL, withLeadingSlash, withQuery } from 'ufo'
44
import { getSiteConfig } from '#site-config/server/composables/getSiteConfig'
55
import { isInternalRoute } from '../../shared'
6+
import { getEventQuery } from '../util/query'
67
import { useOgImageRuntimeConfig } from '../utils'
78

89
// Matches a single <meta> tag and captures property/name and content attributes
@@ -133,7 +134,7 @@ export default defineEventHandler(async (event) => {
133134
})
134135
}
135136

136-
const query = getQuery(event)
137+
const query = getEventQuery(event)
137138
// Pull out resolver-controlled params (prefixed `_og_`) before forwarding the
138139
// rest to the page fetch, so they don't leak into the rendered page's query.
139140
// `_og_key` selects which meta tag to redirect to; accepted case-insensitively

src/runtime/server/util/cache.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { H3Error } from 'h3'
22
import type { OgImageRenderEventContext } from '../../types'
33
import { fnv1a64Base36 } from 'fnv1a-64'
4-
import { createError, getQuery, handleCacheHeaders, setHeader, setHeaders } from 'h3'
4+
import { createError, handleCacheHeaders, setHeader, setHeaders } from 'h3'
55
import { useStorage } from 'nitropack/runtime'
66
import { withTrailingSlash } from 'ufo'
77
import { prefixStorage } from 'unstorage'
88
import { logger } from '../../logger'
9+
import { getEventQuery } from './query'
910

1011
/**
1112
* Constant-time string comparison to prevent timing attacks on secret values.
@@ -76,7 +77,7 @@ export async function useOgImageBufferCache(ctx: OgImageRenderEventContext, opti
7677
: null
7778
if (entry) {
7879
const { value, expiresAt, headers } = entry
79-
const purgeValue = getQuery(ctx.e).purge
80+
const purgeValue = getEventQuery(ctx.e).purge
8081
if (typeof purgeValue !== 'undefined') {
8182
// When URL signing is enabled, require the secret as the purge value
8283
if (options.secret && !safeCompare(String(purgeValue), options.secret)) {

src/runtime/server/util/query.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { H3Event } from 'h3'
2+
import { getQuery } from 'ufo'
3+
4+
/**
5+
* Parse query parameters from the event path without relying on the H3 event
6+
* implementation. Nitro adapters can pass an H3 v1 event to bundled H3 v2
7+
* utilities, where getQuery(event) treats the relative req.url as absolute.
8+
*/
9+
export function getEventQuery(event: H3Event) {
10+
return getQuery(event.path)
11+
}

test/e2e/resolve-endpoint.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ describe('/_og/r resolver endpoint', () => {
3434
expect(res.headers.get('location')).toBeTruthy()
3535
}, 60000)
3636

37+
it('forwards query parameters to the resolved page', async () => {
38+
const pagePath = '/satori/query-param?title=Resolver%20Query'
39+
const pageHtml = await $fetch(pagePath) as string
40+
const expected = extractOgImageUrl(pageHtml)
41+
42+
const res = await fetch(`/_og/r${pagePath}`, { redirect: 'manual' })
43+
expect(res.status).toBe(302)
44+
const location = res.headers.get('location')
45+
expect(location).toBeTruthy()
46+
expect(new URL(location!, 'https://nuxtseo.com').pathname)
47+
.toBe(new URL(expected!, 'https://nuxtseo.com').pathname)
48+
}, 60000)
49+
3750
// The core user-facing scenario from issue #571: a blog/listing page needs
3851
// stable URLs pointing at the og:image of other pages so it can render
3952
// image cards without knowing each page's encoded image URL.

test/unit/event-query.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { H3Event } from 'h3'
2+
import { describe, expect, it } from 'vitest'
3+
import { getEventQuery } from '../../src/runtime/server/util/query'
4+
5+
describe('getEventQuery', () => {
6+
it('parses query parameters from a relative H3 event path', () => {
7+
const relativeUrl = '/_og/r/profile.png?title=Hello%20Nuxt&tag=one&tag=two'
8+
const event = {
9+
path: relativeUrl,
10+
req: { url: relativeUrl },
11+
} as unknown as H3Event
12+
13+
expect(getEventQuery(event)).toEqual({
14+
title: 'Hello Nuxt',
15+
tag: ['one', 'two'],
16+
})
17+
})
18+
})

0 commit comments

Comments
 (0)