Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update handling for relative files in image-optimizer #17998

Merged
merged 14 commits into from Oct 19, 2020
113 changes: 85 additions & 28 deletions packages/next/next-server/server/image-optimizer.ts
@@ -1,4 +1,4 @@
import { UrlWithParsedQuery } from 'url'
import nodeUrl, { UrlWithParsedQuery } from 'url'
import { IncomingMessage, ServerResponse } from 'http'
import { join } from 'path'
import { mediaType } from '@hapi/accept'
Expand All @@ -9,6 +9,7 @@ import { getContentType, getExtension } from './serve-static'
import { fileExists } from '../../lib/file-exists'
// @ts-ignore no types for is-animated
import isAnimated from 'next/dist/compiled/is-animated'
import Stream from 'stream'

let sharp: typeof import('sharp')
//const AVIF = 'image/avif'
Expand All @@ -30,8 +31,6 @@ export async function imageOptimizer(
const { sizes = [], domains = [] } = nextConfig?.images || {}
const { headers } = req
const { url, w, q } = parsedUrl.query
const proto = headers['x-forwarded-proto'] || 'http'
const host = headers['x-forwarded-host'] || headers.host
const mimeType = mediaType(headers.accept, MIME_TYPES) || ''

if (!url) {
Expand All @@ -44,30 +43,31 @@ export async function imageOptimizer(
return { finished: true }
}

let absoluteUrl: URL
try {
absoluteUrl = new URL(url)
} catch (_error) {
// url was not absolute so assuming relative url
let absoluteUrl: URL | undefined
let relativeUrl: string | undefined

if (url.startsWith('/')) {
ijjk marked this conversation as resolved.
Show resolved Hide resolved
relativeUrl = url
} else {
try {
absoluteUrl = new URL(url, `${proto}://${host}`)
} catch (__error) {
absoluteUrl = new URL(url)
} catch (_error) {
res.statusCode = 400
res.end('"url" parameter is invalid')
return { finished: true }
}
}

if (!['http:', 'https:'].includes(absoluteUrl.protocol)) {
res.statusCode = 400
res.end('"url" parameter is invalid')
return { finished: true }
}
if (!['http:', 'https:'].includes(absoluteUrl.protocol)) {
res.statusCode = 400
res.end('"url" parameter is invalid')
return { finished: true }
}

if (!server.renderOpts.dev && !domains.includes(absoluteUrl.hostname)) {
res.statusCode = 400
res.end('"url" parameter is not allowed')
return { finished: true }
if (!server.renderOpts.dev && !domains.includes(absoluteUrl.hostname)) {
ijjk marked this conversation as resolved.
Show resolved Hide resolved
res.statusCode = 400
res.end('"url" parameter is not allowed')
return { finished: true }
}
}

if (!w) {
Expand Down Expand Up @@ -112,7 +112,7 @@ export async function imageOptimizer(
return { finished: true }
}

const { href } = absoluteUrl
const href = (absoluteUrl || relativeUrl) as string
ijjk marked this conversation as resolved.
Show resolved Hide resolved
const hash = getHash([CACHE_VERSION, href, width, quality, mimeType])
const imagesDir = join(distDir, 'cache', 'images')
const hashDir = join(imagesDir, hash)
Expand All @@ -136,17 +136,74 @@ export async function imageOptimizer(
}
}

const upstreamRes = await fetch(href)
let upstreamBuffer: Buffer
let upstreamType: any
ijjk marked this conversation as resolved.
Show resolved Hide resolved
let maxAge: any
ijjk marked this conversation as resolved.
Show resolved Hide resolved

if (absoluteUrl) {
const upstreamRes = await fetch(href)
ijjk marked this conversation as resolved.
Show resolved Hide resolved

if (!upstreamRes.ok) {
res.statusCode = upstreamRes.status
res.end('"url" parameter is valid but upstream response is invalid')
return { finished: true }
}

if (!upstreamRes.ok) {
res.statusCode = upstreamRes.status
res.end('"url" parameter is valid but upstream response is invalid')
return { finished: true }
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer())
upstreamType = upstreamRes.headers.get('Content-Type')
maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
} else {
try {
const _req: any = {
headers: req.headers,
method: req.method,
url: relativeUrl,
}
const resBuffers: Buffer[] = []
const mockRes: any = new Stream.Writable()

mockRes.write = (chunk: Buffer | string) => {
resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
mockRes._write = (chunk: Buffer | string) => {
mockRes.write(chunk)
}

const mockHeaders: Record<string, string | string[]> = {}

mockRes.writeHead = (_status: any, _headers: any) =>
Object.assign(mockHeaders, _headers)
mockRes.getHeader = (name: string) => mockHeaders[name.toLowerCase()]
mockRes.getHeaders = () => mockHeaders
mockRes.getHeaderNames = () => Object.keys(mockHeaders)
mockRes.setHeader = (name: string, value: string | string[]) =>
(mockHeaders[name.toLowerCase()] = value)
mockRes._implicitHeader = () => {}
ijjk marked this conversation as resolved.
Show resolved Hide resolved
mockRes.finished = false
mockRes.statusCode = 200

await server.getRequestHandler()(
_req,
mockRes,
nodeUrl.parse(relativeUrl!, true)
)
res.statusCode = mockRes.statusCode

// make sure to 404 for non-static file requests
if (!mockRes.servedStatic) {
throw new Error('non-static file requested')
ijjk marked this conversation as resolved.
Show resolved Hide resolved
}
upstreamBuffer = Buffer.concat(resBuffers)
upstreamType = mockRes.getHeader('Content-Type')
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'))
} catch (err) {
res.statusCode = 500
res.end('"url" parameter is valid but upstream response is invalid')
return { finished: true }
}
}

const upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer())
const upstreamType = upstreamRes.headers.get('Content-Type')
const maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
const expireAt = maxAge * 1000 + now
let contentType: string

Expand Down
2 changes: 2 additions & 0 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -1692,8 +1692,10 @@ export default class Server {
}

try {
;(res as any).servedStatic = true
await serveStatic(req, res, path)
} catch (err) {
;(res as any).servedStatic = false
if (err.code === 'ENOENT' || err.statusCode === 404) {
this.render404(req, res, parsedUrl)
} else if (err.statusCode === 412) {
Expand Down