microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
65.68k stars 3.57k forks source link

[BUG] Error: apiRequestContext.post: Unexpected buffer type of 'data.file' #23912

Closed paradox37 closed 1 year ago

paradox37 commented 1 year ago

System info

Source code

When I use newContext() and post() api to send multipart form which includes a file, I get the error apiRequestContext.post: Unexpected buffer type of 'data.file'.

I have this fixture:

import {  test as base, APIRequestContext, request } from '@playwright/test';

export const test = base.extend({
  apiRequest: async ({apiBaseURL}, use) => {
    const apiRequestContext = await request.newContext({
      baseURL: process.env.REACT_APP_API_URL
    })
    await use(apiRequestContext);
    await apiRequestContext.dispose();
  },
  post: async ({ apiRequest }, use) => {
    await use(async ({ url, options, context }) => {
      if (options && options.data) {
        options.data = snakecaseKeys(options.data, { exclude: options.exclude || [] });
      }

      if (options && options.multipart) {
        options.multipart = snakecaseKeys(options.multipart, { exclude: options.exclude || [] });
      }

      const response = await apiRequest.post(url, options);

      return camelcaseKeys(await response.json(), { deep: true });
    });
  },
});

Also, second fixture which uses post that relies on newContext() (this is not working, getting error from the title):

import { faker } from '@faker-js/faker';
import { tests as base } from '@fixtures/util/util.fixture.ts';
import snakecaseKeys from 'snakecase-keys';

export const fileTest = base.extend({
  submitFile: async ({ post }, use) => {
    await use(async (data, token) => {
      const { orderId } = data;
      const file = {
        name: 'file.pdf',
        mimeType: 'application/pdf',
        buffer: Buffer.from('file content'),
      };

      const payload = snakecaseKeys({
        orderId
      });

      return post(
        {
          url: someUrl,
          options: {
            multipart: {
              ...payload,
              file,
            },
            headers: {
              'api-token': token,
            },
            exclude: ['mimeType']
          },
        }
      );
    });
  },
});

The same fixture is working if I use context to call post (I would prefer to use newContext() since I don't have to repeat baseUrl every time):

import { faker } from '@faker-js/faker';
import { tests as base } from '@fixtures/util/util.fixture.ts';
import snakecaseKeys from 'snakecase-keys';

export const fileTest = base.extend({
  submitFile: async ({ context }, use) => {
    await use(async (data, token) => {
      const { orderId } = data;
      const file = {
        name: 'file.pdf',
        mimeType: 'application/pdf',
        buffer: Buffer.from('file content'),
      };

      const payload = snakecaseKeys({
        orderId
      });

      return context.request.post(
          url: process.env.REACT_APP_API_URL + someUrl,
          options: {
            multipart: {
              ...payload,
              file,
            },
            headers: {
              'api-token': token,
            },
          },
      );
    });
  },
});

So, it seems newContext() have some issues with multipart form and Buffer.

Expected

Expected multipart form data to be submitted when using newContext()

Actual

Getting the error Error: apiRequestContext.post: Unexpected buffer type of 'data.file' on the line which points to post method in my first fixture.

mxschmitt commented 1 year ago

Looks like you are using unsupported options due to your fixtures not being typed. Look at the following file which has your fixtures in a typed manner:


import { test as base, APIRequestContext, request, expect } from '@playwright/test';

const snakecaseKeys = input => input;
const camelcaseKeys = input => input;

export const test = base.extend<{
  apiRequest: APIRequestContext;
  post: (options: { url: string; options?: Parameters<APIRequestContext['post']>[1] }) => Promise<any>;
  submitFile: (data: { orderId: string }, token: string) => Promise<any>;
}, {
  apiBaseURL: string;
}>({
  apiBaseURL: [process.env.REACT_APP_API_URL!, { scope: 'worker', option: true }],
  apiRequest: async ({ apiBaseURL }, use) => {
    const apiRequestContext = await request.newContext({
      baseURL: process.env.REACT_APP_API_URL
    })
    await use(apiRequestContext);
    await apiRequestContext.dispose();
  },
  post: async ({ apiRequest }, use) => {
    await use(async ({ url, options, context }) => {
      if (options && options.data) {
        options.data = snakecaseKeys(options.data, { exclude: options.exclude || [] });
      }

      if (options && options.multipart) {
        options.multipart = snakecaseKeys(options.multipart, { exclude: options.exclude || [] });
      }

      const response = await apiRequest.post(url, options);

      return camelcaseKeys(await response.json(), { deep: true });
    });
  },
  submitFile: async ({ post }, use) => {
    await use(async (data, token) => {
      const { orderId } = data;
      const file = {
        name: 'file.pdf',
        mimeType: 'application/pdf',
        buffer: Buffer.from('file content'),
      };

      const payload = snakecaseKeys({
        orderId
      });

      return post(
        {
          url: '/api/v1/orders/submit-file',
          options: {
            multipart: {
              ...payload,
              file,
            },
            headers: {
              'api-token': token,
            },
            exclude: ['mimeType']
          },
        }
      );
    });
  },
});

test('test', async ({ submitFile }) => {
  const response = await submitFile({ orderId: '1' }, 'token');

  expect(response).toEqual({ orderId: '1' });
})

With this setup you immediately spot, that you are e.g. using an unsupported option key here:

image

For your specific error, looks like that file.buffer wasn't a Buffer instance, I recommend to add some console logs there to find out what it actually was. (Also use TypeScript if your are not yet).

I wasn't able to replicate your specific error, because this above is not a full reproducible so I had to adjust some things here and there.

paradox37 commented 1 year ago

I am actually using typed fixtures, just didn't put it here, I was taking pieces from the project just to make an example.

How can it not be a buffer instance when I used?:

const file = {
        name: 'file.pdf',
        mimeType: 'application/pdf',
        buffer: Buffer.from('file content'),
      };
mxschmitt commented 1 year ago

Are you able to provide a full reproduction? A full repro with actual/expected outcome would help a ton here to find out why. Without we are unfortunately not able to act on it. Thanks for your understanding.

paradox37 commented 1 year ago

Hm, not sure how to achieve it, I need response from server. Any suggestions for this ?

I now remembered one thing. If server responds with success, this error occurs, but if server responds with validation error, buffer error does not occur. But, everything works fine if newContext() is not used.

mxschmitt commented 1 year ago

The error you shared does not rely on a server response or http endpoint, because it throws already before from here. Would be good to know what you pass to the apiRequestContext.post() function to understand if this is a bug on our side or on your end.

I recommend to put e.g. a try/catch there and log the multipart payload or attach a JavaScript debugger when the exception occurs so you understand wich function passed what.

paradox37 commented 1 year ago

Thanks for pointing me in the right direction. snake-case keys library is causing the issue. It modifies the buffer when using deep conversion. I reported the issue in that library.