Repository: payloadcms/payload
File: packages/storage-r2/src/client/R2ClientUploadHandler.ts (TypeScript)

Code before:
'use client'

import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'

import type {
  R2MultipartUpload,
  R2StorageClientUploadContext,
  R2StorageClientUploadHandlerParams,
  R2StorageMultipartUploadHandlerParams,
  R2UploadedPart,
} from '../types.js'

export const R2ClientUploadHandler = createClientUploadHandler<R2StorageClientUploadHandlerParams>({
  handler: async ({
    apiRoute,
    collectionSlug,
    extra: { chunkSize = 5 * 1024 * 1024, prefix = '' },
    file,
    serverHandlerPath,
    serverURL,
  }): Promise<R2StorageClientUploadContext | undefined> => {
    const endpoint = () =>
      `${serverURL}${apiRoute}${serverHandlerPath}?${new URLSearchParams(params)}`
    const params: R2StorageMultipartUploadHandlerParams = {
      collection: collectionSlug,
      fileName: file.name,
      fileType: file.type,
    }

    const multipart = await fetch(endpoint(), { method: 'POST' })
    if (!multipart.ok) {
      throw new Error('Failed to initialize multipart upload')
    }

    const multipartUpload = (await multipart.json()) as Pick<R2MultipartUpload, 'key' | 'uploadId'>
    const multipartUploadedParts: R2UploadedPart[] = []

    params.multipartId = multipartUpload.uploadId
    params.multipartKey = multipartUpload.key

    const partTotal = Math.ceil(file.size / chunkSize)

    for (let part = 1; part <= partTotal; part++) {
      const bytesEnd = Math.min(part * chunkSize, file.size)
      const bytesStart = (part - 1) * chunkSize

      params.multipartNumber = String(part)


Review comment (suggestion):
Lets use the `formatAdminURL` for this endpoint. You can pass apiRoute to it. This way it will work with basePath configurations.

Code after:
'use client'

import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'
import { formatAdminURL } from 'payload/shared'

import type {
  R2MultipartUpload,
  R2StorageClientUploadContext,
  R2StorageClientUploadHandlerParams,
  R2StorageMultipartUploadHandlerParams,
  R2UploadedPart,
} from '../types.js'

export const R2ClientUploadHandler = createClientUploadHandler<R2StorageClientUploadHandlerParams>({
  handler: async ({
    apiRoute,
    collectionSlug,
    extra: { chunkSize = 5 * 1024 * 1024, prefix = '' },
    file,
    serverHandlerPath,
    serverURL,
  }): Promise<R2StorageClientUploadContext | undefined> => {
    const params: R2StorageMultipartUploadHandlerParams = {
      collection: collectionSlug,
      fileName: file.name,
      fileType: file.type,
    }
    const baseURL = formatAdminURL({
      apiRoute,
      path: serverHandlerPath,
      serverURL,
    })

    const endpoint = `${baseURL}?${String(new URLSearchParams(params))}`

    const multipart = await fetch(endpoint, { method: 'POST' })
    if (!multipart.ok) {
      throw new Error('Failed to initialize multipart upload')
    }

    const multipartUpload = (await multipart.json()) as Pick<R2MultipartUpload, 'key' | 'uploadId'>
    const multipartUploadedParts: R2UploadedPart[] = []

    params.multipartId = multipartUpload.uploadId
    params.multipartKey = multipartUpload.key

    const partTotal = Math.ceil(file.size / chunkSize)