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

Getting "Blob is not defined", "stream.getReader is not a function" and other related errors traced to @aws-sdk code while executing jest test with Amplify App. No such errors occur when executing the app UI. #3443

Closed
mogarick opened this issue Sep 9, 2020 · 11 comments
Labels
guidance Question that needs advice or information.

Comments

@mogarick
Copy link

mogarick commented Sep 9, 2020

SDK version number
1.0.0-gamma.3(?)

I'm not sure. It's the @aws-sdk package that gets added when installing aws-amplify. It's only a dir with many subdirectories, every one of them having its own package.json file. there ones I checked have "version": "1.0.0-gamma.3"

Is the issue in the browser/Node.js/ReactNative?
A mix of Browser/Node.js/ReactNative

Details of the browser/Node.js/ReactNative version
Browsers (I'm testing using chrome for the Expo web version of my app):
Brave Browser: 80.1.5.123
Chrome: 85.0.4183.83
Firefox: 72.0.2
Safari: 13.1.2
Node:
v12.18.3
React Native
The one from Expo 37

TL&DR
I have a React Active Expo + Amplify app. It has a graphql schema that includes @auth directives, with owner and groups support and some with private iam provider permissions.
Testing it manually via UI everything works ok. But using Jest I’m getting errors.
Such errors are related with authentication but they can be traced to components responsible for cyphering (lib-typed-arrays) and request and response processing (Blob, Headers, streamCollector stream.getReader) that I could see varies depending if the mode AWS SDK runs is node, react-native, etc..

I haven’t been able to find how I could control this running mode in order to being able to run my Jest tests without errors.
So I’m not sure if this is a bug or something environment related that can be configured/tweaked somewhere.

Please help me to find a way to solve the problem or a hint about where else to look at.

Troubleshooting story

Problem 1.

Running a test with Jest, where I call Auth.signIn, I got this error:

AuthClass - signIn failure {
      code: 'NotAuthorizedException',
      name: 'NotAuthorizedException',
      message: 'Incorrect username or password.'
    }

After setting Logger.LOG_LEVEL=“DEBUG” and troubleshooting I narrowed the problem to this fragment from crypto-js/lib-typed-arrays used by amazon-cognito-identity-js:

// Handle Uint8Array
	        if (typedArray instanceof Uint8Array) {
	            // Shortcut
	            var typedArrayByteLength = typedArray.byteLength;

	            // Extract bytes
	            var words = [];
	            for (var i = 0; i < typedArrayByteLength; i++) {
	                words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8);
	            }

	            // Initialize this word array
	            superInit.call(this, words, typedArrayByteLength);
	        } else {
	            // Else call normal init
	            superInit.apply(this, arguments);
	        }

the condition fails even though the value of typedArray is an instance of Uint8Array . This only occurs when executing using Jest but not when using my App directly (UI).
I could find the issue aws/aws-encryption-sdk-javascript#126 and with help from a comment on facebook/jest#7780 (comment) I added testEnvironment: “.path/to/my/customenv.js” to the
jest.config.js file. This is the customenv.js file.

"use strict";

const NodeEnvironment = require("jest-environment-node");

class MyEnvironment extends NodeEnvironment {
  constructor(config) {
    super(
      Object.assign({}, config, {
        globals: Object.assign({}, config.globals, {
          Uint32Array: Uint32Array,
          Uint8Array: Uint8Array,
          ArrayBuffer: ArrayBuffer,
        }),
      })
    );
  }

  async setup() {}

  async teardown() {}
}

module.exports = MyEnvironment;

With this tweak the call to Auth.signIn was successful and the test could ran some queries with owners restrictions.

Problem 2

After solving Problem 1, I got a new one. This time when executing a test that requieres an operation with authMode:AWS_IAM. This is where I fell in a rabbit hole. It failed with this error first:

{
    data: {},
    errors: [ GraphQLError { message: 'Request failed with status code 403' } ]
}

Again with the logger activated I found this error as possible cause:

[DEBUG] 01:28.919 Credentials - Error loading credentials ReferenceError: Blob is not defined
        at calculateBodyLength (/path/to/my/project/node_modules/@aws-sdk/util-body-length-browser/src/index.ts:3:5)
        at /path/to/my/project/node_modules/@aws-sdk/middleware-content-length/src/index.ts:28:24
        at step (/path/to/my/project/node_modules/tslib/tslib.js:141:27)
        at Object.next (/path/to/my/project/node_modules/tslib/tslib.js:122:57)
        at /path/to/my/project/node_modules/tslib/tslib.js:115:75
        at tryCallTwo (/path/to/my/project/node_modules/promise/lib/core.js:45:5)
        at doResolve (/path/to/my/project/node_modules/promise/lib/core.js:200:13)
        at new Promise (/path/to/my/project/node_modules/promise/lib/core.js:66:3)
        at Object.__awaiter (/path/to/my/project/node_modules/tslib/tslib.js:111:16)
        at /path/to/my/project/node_modules/@aws-sdk/middleware-content-length/src/index.ts:17:37

Troubleshooting the problem I traced the error to this fragment from @aws-sdk/util-body-length-browser

function calculateBodyLength(body) {
    if (typeof body === "string") {
        return new Blob([body]).size;
    }
    else if (typeof body.byteLength === "number") {
        // handles Uint8Array, ArrayBuffer, Buffer, and ArrayBufferView
        return body.byteLength;
    }
    else if (typeof body.size === "number") {
        // handles browser File object
        return body.size;
    }
}

So it appears the Jest test some how is triggering a browser mode for the AWS SDK, but Jest is running with node mode and Blob is not implemented in node or something like that.
Troubleshooting the problem I followed some ideas from different GitHub issues and SO answers installed blob-polyfill and added it as global to my customenv.js file. So this is how it looked at this point:

"use strict";

const NodeEnvironment = require("jest-environment-node");
const Blob = require("blob-polyfill").Blob;
const debug = require("debug");

class MyEnvironment extends NodeEnvironment {
  constructor(config) {
    super(
      Object.assign({}, config, {
        globals: Object.assign({}, config.globals, {
          Uint32Array: Uint32Array,
          Uint8Array: Uint8Array,
          ArrayBuffer: ArrayBuffer,
          Blob,
        }),
      })
    );
  }

  async setup() {}

  async teardown() {}
}

module.exports = MyEnvironment;

With this tweak the blob problem was gone but the 403 from the query persisted.

Problem 2.1

After solving Problem 2 the new root error was this:

[DEBUG] 29:07.181 Credentials - Failed to load credentials Promise {
      _40: 1,
      _65: 3,
      _55: Promise {
        _40: 0,
        _65: 2,
        _55: ReferenceError: Headers is not defined
            at FetchHttpHandler.Object.<anonymous>.FetchHttpHandler.handle (/path/to/my/project/node_modules/@aws-sdk/fetch-http-handler/src/fetch-http-handler.ts:51:20)
            at /path/to/my/project/node_modules/@aws-sdk/client-cognito-identity/commands/GetIdCommand.ts:45:24
            at Object.<anonymous> (/path/to/my/project/node_modules/@aws-sdk/middleware-serde/src/deserializerMiddleware.ts:16:32)
            at step (/path/to/my/project/node_modules/tslib/tslib.js:141:27)
            at Object.next (/path/to/my/project/node_modules/tslib/tslib.js:122:57)
            at /path/to/my/project/node_modules/tslib/tslib.js:115:75
            at tryCallTwo (/path/to/my/project/node_modules/promise/lib/core.js:45:5)
            at doResolve (/path/to/my/project/node_modules/promise/lib/core.js:200:13)
            at new Promise (/path/to/my/project/node_modules/promise/lib/core.js:66:3)
            at Object.__awaiter (/path/to/my/project/node_modules/tslib/tslib.js:111:16) {
          '$metadata': [Object]
        },
        _72: null
      },
      _72: null
    }

This time I traced the error presumably to this code fragment from @aws-sdk/fetch-http-handler/dist/cjs/fetch-http-handler.js:

FetchHttpHandler.prototype.handle = function (request, options) {
        var abortSignal = options === null || options === void 0 ? void 0 : options.abortSignal;
        var requestTimeoutInMs = this.httpOptions.requestTimeout;
        // if the request was already aborted, prevent doing extra work
        if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
            var abortError = new Error("Request aborted");
            abortError.name = "AbortError";
            return Promise.reject(abortError);
        }
        var path = request.path;
        if (request.query) {
            var queryString = querystring_builder_1.buildQueryString(request.query);
            if (queryString) {
                path += "?" + queryString;
            }
        }
        var port = request.port;
        var url = request.protocol + "//" + request.hostname + (port ? ":" + port : "") + path;
        var requestOptions = {
            body: request.body,
            headers: new Headers(request.headers),
            method: request.method,
        };
        // some browsers support abort signal
        if (typeof AbortController !== "undefined") {
            requestOptions["signal"] = abortSignal;
        }
        var fetchRequest = new Request(url, requestOptions);
        var raceOfPromises = [
            fetch(fetchRequest).then(function (response) {
                var e_1, _a;
                var fetchHeaders = response.headers;
                var transformedHeaders = {};
                try {
                    for (var _b = tslib_1.__values(fetchHeaders.entries()), _c = _b.next(); !_c.done; _c = _b.next()) {
                        var pair = _c.value;
                        transformedHeaders[pair[0]] = pair[1];
                    }
                }
                catch (e_1_1) { e_1 = { error: e_1_1 }; }
                finally {
                    try {
                        if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
                    }
                    finally { if (e_1) throw e_1.error; }
                }
                var hasReadableStream = response.body !== undefined;
                // Return the response with buffered body
                if (!hasReadableStream) {
                    return response.blob().then(function (body) { return ({
                        response: new protocol_http_1.HttpResponse({
                            headers: transformedHeaders,
                            statusCode: response.status,
                            body: body,
                        }),
                    }); });
                }
                // Return the response with streaming body
                return {
                    response: new protocol_http_1.HttpResponse({
                        headers: transformedHeaders,
                        statusCode: response.status,
                        body: response.body,
                    }),
                };
            }),
            request_timeout_1.requestTimeout(requestTimeoutInMs),
        ];
        if (abortSignal) {
            raceOfPromises.push(new Promise(function (resolve, reject) {
                abortSignal.onabort = function () {
                    var abortError = new Error("Request aborted");
                    abortError.name = "AbortError";
                    reject(abortError);
                };
            }));
        }
        return Promise.race(raceOfPromises);
    };

This again is presumably due the fact Jest is running with a node test environment and there is no Headers object there. So I tried to solve it using node-fetch Headers object and adding it to the globals in my customers.js.
This solved the Headers but as the code fragment shows, there’s also a Request object creating, so the new error now was Credentials - Error loading credentials ReferenceError: Request is not defined .
I also added Request from node-fetch to customenv.js
This is the resultant file after the update.

"use strict";

const NodeEnvironment = require("jest-environment-node");
const { Headers, Request } = require("node-fetch");
const Blob = require("blob-polyfill").Blob;
const debug = require("debug");

class MyEnvironment extends NodeEnvironment {
  constructor(config) {
    super(
      Object.assign({}, config, {
        globals: Object.assign({}, config.globals, {
          Uint32Array: Uint32Array,
          Uint8Array: Uint8Array,
          ArrayBuffer: ArrayBuffer,
          Blob,
          Headers,
			Request
        }),
      })
    );
  }

  async setup() {}

  async teardown() {}
}

module.exports = MyEnvironment;

This tweaks solved the Headers and Request errors but the 403 from the query persisted.

Problem 2.2

After solving Problem 2.1 the new root error was:

[DEBUG] 31:23.398 Credentials - Error loading credentials TypeError: url.replace is not a function
        at /path/to/my/project/node_modules/amazon-cognito-identity-js/node_modules/isomorphic-unfetch/index.js:3:36
        at FetchHttpHandler.Object.<anonymous>.FetchHttpHandler.handle (/path/to/my/project/node_modules/@aws-sdk/fetch-http-handler/src/fetch-http-handler.ts:62:7)
        at /path/to/my/project/node_modules/@aws-sdk/client-cognito-identity/commands/GetIdCommand.ts:45:24
        at Object.<anonymous> (/path/to/my/project/node_modules/@aws-sdk/middleware-serde/src/deserializerMiddleware.ts:16:32)
        at step (/path/to/my/project/node_modules/tslib/tslib.js:141:27)
        at Object.next (/path/to/my/project/node_modules/tslib/tslib.js:122:57)
        at /path/to/my/project/node_modules/tslib/tslib.js:115:75
        at tryCallTwo (/path/to/my/project/node_modules/promise/lib/core.js:45:5)
        at doResolve (/path/to/my/project/node_modules/promise/lib/core.js:200:13)
        at new Promise (/path/to/my/project/node_modules/promise/lib/core.js:66:3) {
      '$metadata': { attempts: 1, totalRetryDelay: 0 }
    }

The code fragment where the error occurs is this code from amazon-cognito-identity-js/node_modules/isomorphic-unfetch/index.js

module.exports = global.fetch = global.fetch || (
    typeof process=='undefined' ? (require('unfetch').default || require('unfetch')) : (function(url, opts) {
        return require('node-fetch')(url.replace(/^\/\//g,'https://'), opts);
    })
);

The error occurs when there is no global fetch present and then the last function is executed ( (function(url, opts)…)) because process is not undefined.
This means again AWS SDK is executing presumably in node mode
I noticed wile debugging, that the url param is the Request object so that’s why the error throws. Request doesn’t have a method replace because its not a string.
I hacked a little bit and modified the code to test if(typeof url !== “string” ) and if that’s the case I added url=url.url. Doing that just got me to this other error:

[DEBUG] 53:18.756 Credentials - Error loading credentials TypeError: stream.getReader is not a function
        at /path/to/my/project/node_modules/@aws-sdk/fetch-http-handler/src/stream-collector.ts:21:25
        at step (/path/to/my/project/node_modules/tslib/tslib.js:141:27)
        at Object.next (/path/to/my/project/node_modules/tslib/tslib.js:122:57)
        at /path/to/my/project/node_modules/tslib/tslib.js:115:75
        at tryCallTwo (/path/to/my/project/node_modules/promise/lib/core.js:45:5)
        at doResolve (/path/to/my/project/node_modules/promise/lib/core.js:200:13)
        at new Promise (/path/to/my/project/node_modules/promise/lib/core.js:66:3)
        at Object.__awaiter (/path/to/my/project/node_modules/tslib/tslib.js:111:16)
        at collectStream (/path/to/my/project/node_modules/@aws-sdk/fetch-http-handler/dist/cjs/stream-collector.js:28:20)
        at Object.<anonymous>.exports.streamCollector (/path/to/my/project/node_modules/@aws-sdk/fetch-http-handler/src/stream-collector.ts:10:10) {
      '$metadata': { attempts: 1, totalRetryDelay: 0 }
    }

The same error also appear if I update my customenv.js file to add node-fetch as global.fecth.

The code fragment from @aws-sdk/fetch-http-handler/dist/cjs/stream-collector.js where the error it occurs is this:

exports.streamCollector = function (stream) {
    if (stream instanceof Blob) {
        return collectBlob(stream);
    }
    return collectStream(stream);
};
...
function collectStream(stream) {
    return tslib_1.__awaiter(this, void 0, void 0, function () {
        var res, reader, isDone, _a, done, value, prior;
        return tslib_1.__generator(this, function (_b) {
            switch (_b.label) {
                case 0:
                    res = new Uint8Array(0);
                    reader = stream.getReader();
                    isDone = false;
                    _b.label = 1;
                case 1:
                    if (!!isDone) return [3 /*break*/, 3];
                    return [4 /*yield*/, reader.read()];
                case 2:
                    _a = _b.sent(), done = _a.done, value = _a.value;
                    if (value) {
                        prior = res;
                        res = new Uint8Array(prior.length + value.length);
                        res.set(prior);
                        res.set(value, prior.length);
                    }
                    isDone = done;
                    return [3 /*break*/, 1];
                case 3: return [2 /*return*/, res];
            }
        });
    });
}

Debugging to get here the stream param has a Zlib prototype, so stream instanceof Blob is false and collectStream is called.
The thing here is that this method expects stream to have a getReader method and that’s where the dead ends appears because the stream param doesn’t have such method.
I assume this occurs due to AWS SDK is running in node when I execute my Jest tests and the response object returned is not the same that the one for react-native mode.
In node mode the object returned at the point of error is a Zlib stream. So this is the dead end for me because I think there no easy way to keep tweaking things anymore to work in Jest because it’s not definitively the right path.

The right way to solve the root cause(?)

I think the right way is to make the Jest tests behave in such a way that AWS SDK runs in react-native mode or something like that but I don’t know how to achieve that.
I hope some one can help me out on this or give a hint.

@trivikr trivikr transferred this issue from aws/aws-sdk-js-v3 Sep 9, 2020
@ajredniwja ajredniwja added the guidance Question that needs advice or information. label Sep 9, 2020
@mogarick
Copy link
Author

mogarick commented Sep 10, 2020

@ajredniwja is there a place where I can check how does the AWS SDK determines which of the runtimeConfig files are used? I can see there are :

@aws-sdk/xxxxxxx/runtimeConfig.ts
@aws-sdk/xxxxxxx/runtimeConfig.browser.ts
@aws-sdk/xxxxxxx/runtimeConfig.native.ts
@aws-sdk/xxxxxxx/runtimeConfig.shared.ts

But I can't find out how o where is decided which one to use.

The thing is even though my Jest customenv.js file extends from jest-environment-node, when debugging I can see that in the end CognitoIdentityClient imports a runtimeConfig file that is really runtimeConfig.native. and then everything breaks because of the problems already documented above.

@mogarick
Copy link
Author

I found this article New AWS SDK for JavaScript – Developer Preview where it states

Separate packages for browser and Node.js

...
By publishing separate packages for Node.js and browser environments, we’ve removed the guesswork around which version your builds will use. This also allows us to use environment-specific typings. For example, streams are implemented with different interfaces in Node.js and browsers. Depending on which package you are using, the typings will reflect the streams for the environment you’ve chosen.

Where can I find the way to control which one is used?
That's my problem currently. That I'm running Jest like with node mode but somehow inside amplify & aws-sdk they don't see this and try to use react-native which causes the problem derived from the differences in streams management and so on. And even if I change to extend from jest-environment-jsdom the fetch part continues being the problem because node-fetch is not completely a mirror from Fetch API for web in the streams management and that gets me again in the same problem.

Thank you in advance for the help/hints you can give me.

@ajredniwja
Copy link
Contributor

Hey @mogarick thanks for reaching out. I believe this might be related to V3 but I can try to help you out.

Few things I would want you to do before we proceed further:

@ajredniwja ajredniwja added the response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days. label Sep 10, 2020
@mogarick
Copy link
Author

Thank you @ajredniwja. I'm gonna try out your suggestions. I'll let you know the outcome.

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days. label Sep 12, 2020
@mogarick
Copy link
Author

Hi @ajredniwja. I bumped the aws SDK. The blob problem was indeed solved but the other ones remain. I'm making other changes and tweaks to try to avoid the error. I'll keep the issue updated. I'll also try to implement an isolated code snippet and will share it here when done. Thank you

@jcbdev
Copy link

jcbdev commented Oct 4, 2020

I have this exact issue. No matter how I set up the environment (even with latest aws-sdk) I cannot get past this error when calling Auth.SignIn() in integration tests:

console.log
    [DEBUG] 02:26.792 AuthClass - cannot get cognito credentials TypeError: stream.getReader is not a function
        at /Users/jimbo/Projects/xxxx/node_modules/@aws-sdk/fetch-http-handler/src/stream-collector.ts:23:17
        at step (/Users/jimbo/Projects/xxxx/node_modules/tslib/tslib.js:141:27)
        at Object.next (/Users/jimbo/Projects/xxxx/node_modules/tslib/tslib.js:122:57)
        at /Users/jimbo/Projects/xxxx/node_modules/tslib/tslib.js:115:75
        at new Promise (/Users/jimbo/Projects/xxxx/node_modules/core-js/modules/es.promise.js:233:7)
        at Object.__awaiter (/Users/jimbo/Projects/xxxx/node_modules/tslib/tslib.js:111:16)
        at collectStream (/Users/jimbo/Projects/xxxx/node_modules/@aws-sdk/fetch-http-handler/dist/cjs/stream-collector.js:32:20)
        at Object.exports.streamCollector (/Users/jimbo/Projects/xxxx/node_modules/@aws-sdk/fetch-http-handler/dist/cjs/stream-collector.js:15:12)
        at collectBody (/Users/jimbo/Projects/xxxx/node_modules/@aws-amplify/core/node_modules/@aws-sdk/client-cognito-identity/protocols/Aws_json1_1.ts:3388:18)
        at collectBodyString (/Users/jimbo/Projects/xxxx/node_modules/@aws-amplify/core/node_modules/@aws-sdk/client-cognito-identity/protocols/Aws_json1_1.ts:3393:3) {
      '$metadata': { attempts: 1, totalRetryDelay: 0 }
    }

      at ConsoleLogger._log (node_modules/@aws-amplify/core/src/Logger/ConsoleLogger.ts:99:4)

@jcbdev
Copy link

jcbdev commented Oct 6, 2020

@mogarick I found a temporary work around for my tests. I have a typescript setup file (jest.setup.ts) and I added the following code to set up a "monkeypatch" for @aws-sdk/fetch-http-handler library. All my tests now pass. I basically changed the collectStream function to work on a node readable stream instead of a whatwg readablestream

import { StreamCollector } from '@aws-sdk/types';
import 'cross-fetch/polyfill';

jest.mock('@aws-sdk/fetch-http-handler', () => {
  const { BrowserHttpOptions, FetchHttpHandler } = jest.requireActual(
    '@aws-sdk/fetch-http-handler',
  );

  const newStreamCollector: StreamCollector = (
    stream: any,
  ): Promise<Uint8Array> => {
    return collectStream(stream);
  };

  async function collectStream(
    stream: NodeJS.ReadableStream,
  ): Promise<Uint8Array> {
    return new Promise((resolve, reject) => {
      let res = new Uint8Array(0);
      stream.on('data', (chunk) => {
        const prior = res;
        res = new Uint8Array(prior.length + chunk.length);
        res.set(prior);
        res.set(chunk, prior.length);
      });
      stream.on('end', function () {
        resolve(res);
      });
      stream.on('error', function (err) {
        reject(err);
      });
    });
  }

  return {
    streamCollector: newStreamCollector,
    BrowserHttpOptions,
    FetchHttpHandler,
  };
});

@ajredniwja
Copy link
Contributor

@jcbdev thanks for providing a workaround, @mogarickI would close this issue for now, please reach out if there are additional questions.

@SourceCode
Copy link

SourceCode commented Jan 21, 2021

We have rolled up all our packages to latest versions and this problem persists for S3. In our case we are using Cypress for automated testing and the only area that is impacted is S3.

The below screenshot happens after automation has already authenticated, navigated to an area, and then we are using cypress to attach a file upload for testing. The result is it attempts to load these credentials again and fails on the streamReader.

This is exposed via window.LOG_LEVEL = 'DEBUG';

We are using version v2.829.0 of the aws-sdk within the project.

You can see it here:
s3-cypress-untimely-failures-lol

@kevcam4891
Copy link

kevcam4891 commented May 19, 2021

@SourceCode Did you ever find a solution to this? This too affects our Cypress testing and we cannot get past it.

@warrenronsiek
Copy link

Problem still exists. IMO this shouldn't have been closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
guidance Question that needs advice or information.
Projects
None yet
Development

No branches or pull requests

6 participants