Tricky Problem Using OpenApi-Fetch, ServerConnection.makeRequest and Chrome

I’m primarily posting this to share a very obscure problem I ran into that was hard to workaround - and there might be a better workaround.

Context - I am developing an extension, which includes a server extension and JupyterLab frontend extension, which talks to endpoints on the server. I wanted to try and ensure the frontend and backend stay in sync.

I then tried using openapi-typescript and openapi-fetch to generate schema for this communication and to perform validation on requests.

openapi-fetch creates a typesafe client object which can perform requests and validation for you.

However, because of the way JupyterLab secures requests, in order for them to be received by the server, these need to be mediated via ServerConnection.makeRequest which fills in the required XSRF etc. stuff.

This function has a similar API to fetch and so I tried creating a wrapper JLfetch function to pass requests to makeRequest and then configured openapi-fetch to call this instead of the regular fetch.

The problem I encountered was that openapi-fetch creates a Request object prior to using the configured fetch function. Seemingly in Chrome, but not in Firefox (which is what I primarily use for prototyping), when one creates a Request option for a POST request with a body, it converts the request body into a ReadableStream. I was then passing this ready-made request into ServerConnection.makeRequest, but because the body had been turned into a ReadableStream at the point of making the request, Chrome complains that duplex must be set for requests of this type. This same error may occur in other browsers, I’m not sure.

As a workaround for this my JLfetch function now looks like:

import createClient from 'openapi-fetch';
import { paths } from './schema/schema';

const JLfetch = async (info: Request) => {
  const url = info.url;
  const { method, body } = info;
  const init: RequestInit = { method, body };

  // seems in some browsers, body is turned into a stream
  // see https://issues.chromium.org/issues/40237822#makechanges
  if (body instanceof ReadableStream ) {
    init.body = await info.text() // turn body back into a string
  }
  
  const settings = ServerConnection.makeSettings();  
  return ServerConnection.makeRequest(url, init, settings);
};

const settings = ServerConnection.makeSettings();

export const client = createClient<paths>({
  baseUrl: URLExt.join(settings.baseUrl, 'jupyter_cassini'),
  fetch: JLfetch
});

Which converts the ReadableStream back into a string, before passing it to makeRequest.

Which seems to be working.

The way ServerConnection.makeRequest is currently implemented makes it hard to create your own Request object with all the appropriate stuff to be valid, hence such a workaround is currently necessary.

I hope this might be helpful to someone at some point!

Caveat here is that I’m not super familiar with web development, so I could be doing something very dumb here.

Cheers,

Hugh

2 Likes