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

Unable to upload image asset (as Blob) from NextJS Route Handler #135

Open
benderillo opened this issue Feb 24, 2023 · 13 comments
Open

Unable to upload image asset (as Blob) from NextJS Route Handler #135

benderillo opened this issue Feb 24, 2023 · 13 comments
Labels

Comments

@benderillo
Copy link

When trying to upload an image as Blob using the client.assets.upload method inside the Nextjs 13 Route Handler, the method throws error:

Error: Request body must be a string, buffer or stream, got object
    at httpRequester (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:468:15)
    at Object.eval (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:134:30)
    at Object.publish (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:80:28)
    at Observable.eval [as _subscribe] (webpack-internal:///(sc_server)/./node_modules/get-it/dist/middleware.cjs:311:34)
    at Observable._trySubscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:36:25)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:121)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at Observable.eval [as _subscribe] (webpack-internal:///(sc_server)/./node_modules/@sanity/client/dist/index.cjs:815:51)
    at Observable._trySubscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:36:25)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:121)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/operators/filter.js:11:16)
    at OperatorSubscriber.eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/lift.js:16:28)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:48)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/operators/map.js:11:16)
    at SafeSubscriber.eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/lift.js:16:28)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:48)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/lastValueFrom.js:12:16)
    at new Promise (<anonymous>)
    at Object.lastValueFrom (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/lastValueFrom.js:9:12)
    at AssetsClient.upload (webpack-internal:///(sc_server)/./node_modules/@sanity/client/dist/index.cjs:901:21)

The code is as following:

import { createClient } from '@sanity/client';
const client = createClient({
  projectId: 'sdfsd',
  dataset: 'sdfdsf,
  apiVersion: '2023-02-23',
  useCdn: false,
  token: process.env.SANITY_TOKEN,
});

The route handler file /app/api/myform/route.ts

import { type NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';

export async function POST(req: NextRequest) {
   const form = await req.formData();
   const file = form.get('file') as Blob;

   if (!file) {
      return new NextResponse('Bad request', { status: 400 });
   }

   await client.assets.upload('image', file, { filename: 'image'});
   return NextResponse.json({});
}

I checked that the blob is valid and has the proper image file.
I guess the get-it package under the hood always expects Nodejs type of body (not the browser/Web API)

There is a duplicate issue submitted for the same problem on next-sanity repo sanity-io/next-sanity#348

@imchivaa
Copy link

Hi there, i am facing the same issue, i am using pdfMake to spill out the blob and sending it as part of the client's upload function throws the same error:

Upload failed: Error: Request body must be a string, buffer or stream, got object

const invoiceTemplateData = await getInvoiceTemplate(result.invoice.uniqueReferenceNumber);
                const invoiceJson = JSON.parse(invoiceTemplateData);
                const pdf = pdfMake.createPdf(invoiceJson.data);
                pdf.getBlob((blob) => {
                    createClient.assets
                        .upload('file', blob, {
                            filename: `${customer?.name} - ${result.invoice.uniqueReferenceNumber}.pdf`,
                            title: `${customer?.name} - ${result.invoice.uniqueReferenceNumber}`
                        }).then((document) => {
                            console.log('====================================')
                            console.log('The file was uploaded!', document)
                            console.log('====================================')
                        })
                        .catch((error) => {
                            console.log('====================================')
                            console.error('Upload failed:', error)
                            console.log('====================================')
                        })
                });

It says body can contain string, but when i send a string part of the body, it says function can't accept string.

Did you manage to solve this? Mind sharing how you solve this problem? Thanks.

@benderillo
Copy link
Author

I did not solve the problem and I don't think I can.
This should be fixed by Sanity team or whoever maintains the javascript library.
I am surprised they state the library is "Edge" compatible in the docs but it is not true.

More disappointing is the fact that this issue is open for three weeks already but there was not a single comment yet whether there is any plan to address it.
With Next 13 released almost half a year ago and "Edge" growing rapidly, this is concerning.

@imchivaa
Copy link

imchivaa commented Mar 19, 2023

Hi there,

Ya, I also got frustrated, anyhow, I managed to get it work with below approach, not sure if this will help you.

I first get the base64 string, since i use pdfMake, i can convert to buffer as below:

pdf.getBase64((base64) => {
const bufferValue = Buffer.from(base64, "base64"); // this will get the buffer value, then can be passed to the upload function.
});

Below is the file upload function:

createClient.assets
    .upload('file', bufferValue, {
        filename: `${customer?.name} - ${result.invoice.uniqueReferenceNumber}.pdf`,
        title: `${customer?.name} - ${result.invoice.uniqueReferenceNumber}`
    }).then((file) => {
        console.log('====================================')
        console.log('The file was uploaded!', file)
        console.log('====================================')
    })
    .catch((error) => {
        console.log('====================================')
        console.error('Upload failed:', error)
        console.log('====================================')
    })
});

Let me know if it works for you. :)

@stipsan
Copy link
Member

stipsan commented Mar 23, 2023

Hi 👋
Will follow up here: #176

@benderillo
Copy link
Author

@stipsan thank you for looking into this.
I have one more question about the library (NextJs 13 related)
Right now it is not possible to use this library on SSR pages that have the rendering on the edge enabled (export const runtime = 'experimental-edge') because build fails with the following error:

./node_modules/get-it/dist/middleware.browser.cjs:162:24
Module not found: Can't resolve 'buffer'

https://nextjs.org/docs/messages/module-not-found

Import trace for requested module:
./node_modules/@sanity/client/dist/index.browser.cjs

Is it something that will be also addressed by the #176 ?

@stipsan
Copy link
Member

stipsan commented Mar 23, 2023

It might be addressed by #176, but I'll guess it probably won't since I can see it's loading the ./dist/middleware.browser.cjs file.
With edge-light it should change to ./dist/middleware.browser.js but the only difference between the two are ESM versus CJS syntax.
As long as it's a middleware.browser file it's supposed to be free of any of the node globals 🤔

You can test this before #176 lands though by npm install get-it to force update to v8.1.0 so you'll get the edge-light fix.

@stipsan
Copy link
Member

stipsan commented Mar 23, 2023

I'm perplexed, I can't find any 'buffer' reference in here: https://unpkg.com/[email protected]/dist/middleware.browser.cjs or here https://unpkg.com/[email protected]/dist/middleware.browser.cjs 🤔 🤔

Can you show me what line 162 and its surroundings looks like @benderillo ?

@benderillo
Copy link
Author

Here is an excerpt of contents of the middleware.browser.cjs

function injectResponse() { // **<== LINE 138**
  let opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  if (typeof opts.inject !== "function") {
    throw new Error("`injectResponse` middleware requires a `inject` function");
  }
  function inject(prevValue, event) {
    const response = opts.inject(event, prevValue);
    if (!response) {
      return prevValue;
    }
    const options = event.context.options;
    return Object.assign({}, {
      body: "",
      url: options.url,
      method: options.method,
      headers: {},
      statusCode: 200,
      statusMessage: "OK"
    }, response);
  }
  return {
    interceptRequest: inject
  };
}
const isBuffer = typeof Buffer === "undefined" ? () => false : obj => Buffer.isBuffer(obj);   // **<=== LINE 162**
const serializeTypes = ["boolean", "string", "number"];
function jsonRequest() {
  return {
    processOptions: options => {
      const body = options.body;
      if (!body) {
        return options;
      }
      const isStream = typeof body.pipe === "function";
      const shouldSerialize = !isStream && !isBuffer(body) && (serializeTypes.indexOf(typeof body) !== -1 || Array.isArray(body) || isPlainObject.isPlainObject(body));
      if (!shouldSerialize) {
        return options;
      }
      return Object.assign({}, options, {
        body: JSON.stringify(options.body),
        headers: Object.assign({}, options.headers, {
          "Content-Type": "application/json"
        })
      });
    }
  };
} // **<== line 184**

Screen Shot 2023-03-24 at 2 29 05 PM

Here is the version info from yarn.lock

get-it@^8:
  version "8.0.11"
  resolved "https://registry.yarnpkg.com/get-it/-/get-it-8.0.11.tgz#c489c46eba3e7b94e411e4e831ae34696662086c"
  integrity sha512-x7AlRWAWJKDve52Sk9Nkki1Ne9TwH2cZ4KknCswya88nM6IGoK/kDKv+FKAzBZ1QzhZGqTaqb+qzrectzYdTHw==
  dependencies:
    debug "^4.3.4"
    decompress-response "^7.0.0"
    follow-redirects "^1.15.2"
    into-stream "^6.0.0"
    is-plain-object "^5.0.0"
    is-retry-allowed "^2.2.0"
    is-stream "^2.0.1"
    parse-headers "^2.0.5"
    progress-stream "^2.0.0"
    tunnel-agent "^0.6.0"
"@sanity/client@^5.3.0":
  version "5.3.0"
  resolved "https://registry.yarnpkg.com/@sanity/client/-/client-5.3.0.tgz#10a3ec770c5c17acf19c18c0f5354a1b7ab869af"
  integrity sha512-GUu33gDy5Lx9FNyRGeRNrn2ZYNNthn3Qj2y4GPtb3MkdUtdWHKXmCi3S7nqusTshwS0Uz/w+wX9/h5Nbq9iofg==
  dependencies:
    "@sanity/eventsource" "^4"
    get-it "^8"
    rxjs "^7"

@stipsan
Copy link
Member

stipsan commented Mar 28, 2023

@benderillo yeah I can see a reference to the Buffer global sure, but that alone shouldn't trigger the error you're seeing.
I would expect that error if we were doing something like:

import Buffer from 'buffer'

anywhere in the code we're shipping. Which means there's something happening in the NextJS pipeline that attempts to polyfill Buffer even though it's unnecessary 🤔

@stipsan
Copy link
Member

stipsan commented Mar 28, 2023

We have shipped #176 https://github.com/sanity-io/client/releases/tag/v5.4.0 and perhaps it's enough to trigger the right behavior in the Edge runtime 🙇

@benderillo
Copy link
Author

@am79041 your comment is off-topic.
The issue is specifically about using Blob as an input to client.assets.upload().
First of all, suggesting to use stream instead is like saying "well don't use it the way you do" instead of fixing it.
Secondly, the issue is about using this API in Route Handler. Route handlers do not have NodeJs types (including NodeJs Streams) hence the Blob and form data are used.
Please refrain from posting irrelevant code and comments because it distracts anyone later reading through the thread if they come across this issue.

@kmelve
Copy link
Member

kmelve commented Apr 11, 2023

@benderillo and @am79041: I sense some frustration on both sides in your interaction above here. While we’re trying to figure out what’s going on and hopefully be able to help, I ask you to consider that other folks are reading this thread as well and that kindness goes a long way. Thanks for understanding!

@stipsan
Copy link
Member

stipsan commented Apr 12, 2023

Just a quick update, we're tracking this issue as part of a larger problem in get-it where it's not supporting web idl features like FormData, File, ArrayBuffer and Blob when using the node env. Beyond next we've seen this affect astro users and others.

A possible workaround, while we look into this and work on a proper fix, is to use edge, deno or cloudflare-worker runtimes as get-it will in those runtimes defer to the global fetch API just like on browsers with how to handle the request body formats.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants