i18next / i18next-http-backend

i18next-http-backend is a backend layer for i18next using in Node.js, in the browser and for Deno.
MIT License
443 stars 67 forks source link

Cloudflare Worker/Pages endless init #96

Closed Dav1dde closed 1 year ago

Dav1dde commented 1 year ago

🐛 Bug Report

Awaiting the result of init() never finishes on Cloudflare Workers. There is no exception, it simply does never finish.

To Reproduce

The following code displays the behaviour:

    const instance = createInstance();
    await instance
        .use(initReactI18next)
        .use(Backend)
        .init({
            ns: ['common'],
            lng: 'en-US',
            react: {suspense: false},
            backend: {
                loadPath: `${baseUrl}${i18n.backend.loadPath}`
            }
        });

My assumption is that there is an exception outside the fetch handler/promise and that this causes the passed in callback here: https://github.com/i18next/i18next-http-backend/blob/84c188dce4353d639b64f66831d579aaa6ee2b73/lib/request.js#L109 to never be called, hence never completing the promise returned from init.

Using a very simplified handler, solves the problem:

            backend: {
                loadPath: `${baseUrl}${i18n.backend.loadPath}`,
                request: (options, url, payload, callback) => {
                    fetch(url).then(response => response.text()).then(data => callback(null, { status: response.status, data })
                }
            }

Unfortunately I couldnt debug this further where it crashes. I haven't figured out how I would find such exception yet, because this does work locally with wrangler dev/wrangler pages dev (the local test env for Cloudflare Workers).

Runtime: Cloudflare Workers Version: 1.4.1

adrai commented 1 year ago

Can you set debug to true and check if there is something useful in the logs?

adrai commented 1 year ago

Can you try with v1.4.2?

Dav1dde commented 1 year ago

I left my laptop at work, will try 1.4.2 first thing in the morning!

Dav1dde commented 1 year ago

Unfortunately this did not solve the issue. I'll have to see how I can debug this, since I can't even access the logs.

Dav1dde commented 1 year ago

This makes no sense. Setting requestOptions to {} fixes the issue.

Somehow fetch completely goes off the rails when passed an invalid field in the RequestInit. I tried selectively trimming the list of builtin requestOptions and if any of these are present the fetch just fails and never completes (or returns).

I would call this a Cloudflare Bug, not much you can do here. Thanks for your help.

adrai commented 1 year ago

In case you have an idea on how we could optimize, let me know...

Dav1dde commented 1 year ago

Okay, a little bit more debugging, fetch doesn't silently fail, it simply throws an error:

Error: The 'mode' field on 'RequestInitializerDict' is not implemented.
    at Li (worker.mjs:1568:18)
    at next (worker.mjs:1952:32)
    at Object.fetch (worker.mjs:1966:14)

The only thing I can think of you could be doing, is to try and detect the environment and remove the default options when you're in a CF Worker.

It also would make sense to call the callback in case this happens, then at least the promise resolves.

Re-Opening just in case you want to investigate.

adrai commented 1 year ago

Do you know of a reliable way to detect the CF worker runtime?

adrai commented 1 year ago

Else we could do something like this:

const fetchIt = (url, fetchOptions, callback) => {
  fetchApi(url, fetchOptions).then((response) => {
    if (!response.ok) return callback(response.statusText || 'Error', { status: response.status })
    response.text().then((data) => {
      callback(null, { status: response.status, data })
    }).catch(callback)
  }).catch(callback)
}

// fetch api stuff
const requestWithFetch = (options, url, payload, callback) => {
  if (options.queryStringParams) {
    url = addQueryString(url, options.queryStringParams)
  }
  const headers = defaults({}, typeof options.customHeaders === 'function' ? options.customHeaders() : options.customHeaders)
  if (payload) headers['Content-Type'] = 'application/json'
  const fetchOptions = defaults({
    method: payload ? 'POST' : 'GET',
    body: payload ? options.stringify(payload) : undefined,
    headers
  }, typeof options.requestOptions === 'function' ? options.requestOptions(payload) : options.requestOptions)
  try {
    fetchIt(url, fetchOptions, callback)
  } catch (e) {
    if (!fetchOptions.mode || !e.message || e.message.indexOf('mode') < 0 || e.message.indexOf('not implemented') < 0) {
      return callback(e)
    }
    try {
      delete fetchOptions.mode
      fetchIt(url, fetchOptions, callback)
    } catch (err) {
      callback(err)
    }
  }
}
Dav1dde commented 1 year ago

Do you know of a reliable way to detect the CF worker runtime?

There is this: https://developers.cloudflare.com/workers/platform/compatibility-dates/#global-navigator navigator.userAgent === "Cloudflare-Workers" But this does not work for everything (yet).

Maybe check when navigator exists (ReferenceError: navigator is not defined in my env), if it doesn't exist, maybe just a thing to document.

adrai commented 1 year ago

Can you try with v1.4.3?

Dav1dde commented 1 year ago

Sorry I should have been more clear, the problem happens with all additional fields, not just mode, credentials and cache are also problematic and have the same behavior. Here is the entire list of allowed fields: https://developers.cloudflare.com/workers//runtime-apis/request#requestinit

adrai commented 1 year ago

v1.4.4? ;-)

Dav1dde commented 1 year ago

Awesome, 1.4.4 works! But if I see that correctly, it does also remove custom requestOptions? E.g. if I wanted to specify custom cache rules, I would need to add a cf: {...} object as requestOptions.

Dav1dde commented 1 year ago

Nevermind, it doesn't. Because if I specify them, the first call never fails.

Dav1dde commented 1 year ago

Thank you very much, this seems to work perfectly now!

adrai commented 1 year ago

If you like this module don’t forget to star this repo. Make a tweet, share the word or have a look at our https://locize.com to support the devs of this project.

There are many ways to help this project 🙏