11import { NextRequest , NextResponse } from "next/server"
2- import { makeAPIErrorResponse , makeUnauthenticatedAPIErrorResponse } from "@/common"
2+ import { env , makeAPIErrorResponse , makeUnauthenticatedAPIErrorResponse } from "@/common"
33import { session } from "@/composition"
44
5+ const ErrorName = {
6+ MAX_FILE_SIZE_EXCEEDED : "MaxFileSizeExceededError" ,
7+ TIMEOUT : "TimeoutError"
8+ }
9+
510export async function GET ( req : NextRequest ) {
611 const isAuthenticated = await session . getIsAuthenticated ( )
712 if ( ! isAuthenticated ) {
@@ -17,6 +22,63 @@ export async function GET(req: NextRequest) {
1722 } catch {
1823 return makeAPIErrorResponse ( 400 , "Invalid \"url\" query parameter." )
1924 }
20- const file = await fetch ( url ) . then ( r => r . blob ( ) )
21- return new NextResponse ( file , { status : 200 } )
25+ try {
26+ const maxMegabytes = Number ( env . getOrThrow ( "PROXY_API_MAXIMUM_FILE_SIZE_IN_MEGABYTES" ) )
27+ const timeoutInSeconds = Number ( env . getOrThrow ( "PROXY_API_TIMEOUT_IN_SECONDS" ) )
28+ const maxBytes = maxMegabytes * 1024 * 1024
29+ const file = await downloadFile ( { url, maxBytes, timeoutInSeconds } )
30+ return new NextResponse ( file , { status : 200 } )
31+ } catch ( error ) {
32+ if ( error instanceof Error == false ) {
33+ return makeAPIErrorResponse ( 500 , "An unknown error occurred." )
34+ }
35+ if ( error . name === ErrorName . MAX_FILE_SIZE_EXCEEDED ) {
36+ return makeAPIErrorResponse ( 413 , "The operation was aborted." )
37+ } else if ( error . name === ErrorName . TIMEOUT ) {
38+ return makeAPIErrorResponse ( 408 , "The operation timed out." )
39+ } else {
40+ return makeAPIErrorResponse ( 500 , error . message )
41+ }
42+ }
43+ }
44+
45+ async function downloadFile ( params : {
46+ url : URL ,
47+ maxBytes : number ,
48+ timeoutInSeconds : number
49+ } ) : Promise < Blob > {
50+ const { url, maxBytes, timeoutInSeconds } = params
51+ const abortController = new AbortController ( )
52+ const timeoutSignal = AbortSignal . timeout ( timeoutInSeconds * 1000 )
53+ const response = await fetch ( url , {
54+ signal : AbortSignal . any ( [ abortController . signal , timeoutSignal ] )
55+ } )
56+ if ( ! response . body ) {
57+ throw new Error ( "Response body unavailable" )
58+ }
59+ let totalBytes = 0
60+ let didExceedMaxBytes = false
61+ const reader = response . body . getReader ( )
62+ const chunks : Uint8Array [ ] = [ ]
63+ // eslint-disable-next-line no-constant-condition
64+ while ( true ) {
65+ // eslint-disable-next-line no-await-in-loop
66+ const { done, value } = await reader . read ( )
67+ if ( done ) {
68+ break
69+ }
70+ totalBytes += value . length
71+ chunks . push ( value )
72+ if ( totalBytes >= maxBytes ) {
73+ didExceedMaxBytes = true
74+ abortController . abort ( )
75+ break
76+ }
77+ }
78+ if ( didExceedMaxBytes ) {
79+ const error = new Error ( "Maximum file size exceeded" )
80+ error . name = ErrorName . MAX_FILE_SIZE_EXCEEDED
81+ throw error
82+ }
83+ return new Blob ( chunks )
2284}
0 commit comments