Skip to content

Commit 3a2035c

Browse files
committed
fix(cloudflare): handle app.baseURL, cross-zone origin detection and external sources
1 parent 67b40ac commit 3a2035c

2 files changed

Lines changed: 134 additions & 10 deletions

File tree

src/runtime/providers/cloudflare.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encodeQueryItem, joinURL } from 'ufo'
1+
import { encodeQueryItem, hasProtocol, joinURL } from 'ufo'
22
import { createOperationsGenerator } from '../utils/index'
33
import { defineProvider } from '../utils/provider'
44

@@ -36,20 +36,43 @@ interface CloudflareOptions {
3636
baseURL?: string
3737
}
3838

39-
// https://developers.cloudflare.com/images/image-resizing/url-format/
39+
function getRequestOrigin(event: unknown): string {
40+
const headers = (event as any)?.headers
41+
if (typeof headers?.get === 'function') {
42+
const host = headers.get('x-forwarded-host') || headers.get('host')
43+
const proto = headers.get('x-forwarded-proto') || 'https'
44+
if (host) return `${proto}://${host}`
45+
}
46+
if (typeof window !== 'undefined' && window.location?.origin && window.location.origin !== 'null') {
47+
return window.location.origin
48+
}
49+
return ''
50+
}
51+
52+
// https://developers.cloudflare.com/images/transform-images/transform-via-url/
4053
export default defineProvider<CloudflareOptions>({
41-
getImage: (src, {
42-
modifiers,
43-
baseURL = '/',
44-
}) => {
54+
getImage: (src, { modifiers, baseURL = '/' }, ctx) => {
4555
const mergeModifiers = { ...defaultModifiers, ...modifiers }
4656
const operations = operationsGenerator(mergeModifiers as any)
4757

48-
// https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>
49-
const url = operations ? joinURL(baseURL, 'cdn-cgi/image', operations, src) : src
58+
const isExternal = hasProtocol(src)
59+
const sourcePath = isExternal ? src : joinURL(ctx.options.nuxt.baseURL, src)
5060

51-
return {
52-
url,
61+
// When baseURL is a different zone (absolute URL) and src is relative,
62+
// resolve to an absolute URL so Cloudflare can fetch from the correct origin
63+
let imageSource = sourcePath
64+
if (!isExternal && hasProtocol(baseURL)) {
65+
const origin = getRequestOrigin(ctx.options.event)
66+
if (origin) {
67+
imageSource = joinURL(origin, sourcePath)
68+
}
5369
}
70+
71+
// https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>
72+
const url = operations
73+
? joinURL(baseURL, 'cdn-cgi/image', operations, imageSource)
74+
: sourcePath
75+
76+
return { url }
5477
},
5578
})

test/nuxt/providers.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,107 @@ describe('Providers', () => {
109109
}
110110
})
111111

112+
it('cloudflare with app.baseURL', () => {
113+
const ctx = { options: { ...emptyContext.options, nuxt: { baseURL: '/admin/' } } } as any
114+
115+
expect(cloudflare().getImage('/images/test.png', {
116+
modifiers: { width: 200 },
117+
baseURL: '/',
118+
}, ctx)).toMatchObject({ url: '/cdn-cgi/image/w=200/admin/images/test.png' })
119+
120+
expect(cloudflare().getImage('/images/test.png', {
121+
modifiers: {},
122+
baseURL: '/',
123+
}, ctx)).toMatchObject({ url: '/admin/images/test.png' })
124+
})
125+
126+
it('cloudflare with external image', () => {
127+
expect(cloudflare().getImage('https://example.com/photo.jpg', {
128+
modifiers: { width: 200 },
129+
baseURL: '/',
130+
}, emptyContext)).toMatchObject({ url: '/cdn-cgi/image/w=200/https://example.com/photo.jpg' })
131+
132+
expect(cloudflare().getImage('https://example.com/photo.jpg', {
133+
modifiers: {},
134+
baseURL: '/',
135+
}, emptyContext)).toMatchObject({ url: 'https://example.com/photo.jpg' })
136+
})
137+
138+
it('cloudflare cross-zone', () => {
139+
const ctx = {
140+
options: {
141+
...emptyContext.options,
142+
nuxt: { baseURL: '/' },
143+
event: {
144+
headers: new Headers({
145+
'host': 'app.example.com',
146+
'x-forwarded-proto': 'https',
147+
}),
148+
},
149+
},
150+
} as any
151+
152+
expect(cloudflare().getImage('/images/test.png', {
153+
modifiers: { width: 200 },
154+
baseURL: 'https://cdn.example.com',
155+
}, ctx)).toMatchObject({ url: 'https://cdn.example.com/cdn-cgi/image/w=200/https://app.example.com/images/test.png' })
156+
157+
expect(cloudflare().getImage('/images/test.png', {
158+
modifiers: {},
159+
baseURL: 'https://cdn.example.com',
160+
}, ctx)).toMatchObject({ url: '/images/test.png' })
161+
})
162+
163+
it('cloudflare cross-zone with app.baseURL', () => {
164+
const ctx = {
165+
options: {
166+
...emptyContext.options,
167+
nuxt: { baseURL: '/admin/' },
168+
event: {
169+
headers: new Headers({
170+
'host': 'app.example.com',
171+
'x-forwarded-proto': 'https',
172+
}),
173+
},
174+
},
175+
} as any
176+
177+
expect(cloudflare().getImage('/images/test.png', {
178+
modifiers: { width: 200 },
179+
baseURL: 'https://cdn.example.com',
180+
}, ctx)).toMatchObject({ url: 'https://cdn.example.com/cdn-cgi/image/w=200/https://app.example.com/admin/images/test.png' })
181+
182+
expect(cloudflare().getImage('/images/test.png', {
183+
modifiers: {},
184+
baseURL: 'https://cdn.example.com',
185+
}, ctx)).toMatchObject({ url: '/admin/images/test.png' })
186+
})
187+
188+
it('cloudflare cross-zone with external src', () => {
189+
const ctx = {
190+
options: {
191+
...emptyContext.options,
192+
nuxt: { baseURL: '/' },
193+
event: {
194+
headers: new Headers({
195+
'host': 'app.example.com',
196+
'x-forwarded-proto': 'https',
197+
}),
198+
},
199+
},
200+
} as any
201+
202+
expect(cloudflare().getImage('https://other.example.com/images/test.png', {
203+
modifiers: { width: 200 },
204+
baseURL: 'https://cdn.example.com',
205+
}, ctx)).toMatchObject({ url: 'https://cdn.example.com/cdn-cgi/image/w=200/https://other.example.com/images/test.png' })
206+
207+
expect(cloudflare().getImage('https://other.example.com/images/test.png', {
208+
modifiers: {},
209+
baseURL: 'https://cdn.example.com',
210+
}, ctx)).toMatchObject({ url: 'https://other.example.com/images/test.png' })
211+
})
212+
112213
it('cloudinary', () => {
113214
const providerOptions = {
114215
baseURL: '/',

0 commit comments

Comments
 (0)