Amplify-js: Method get of Storage is not supported for SSR

Created on 6 Jan 2021  ยท  5Comments  ยท  Source: aws-amplify/amplify-js

Describe the bug
I'm trying to get an image using the method get from Storage, like this:

export const getServerSideProps: GetServerSideProps = async ({
  params,
  req,
}) => {
  const { id } = params;
  const SSR = withSSRContext({ req });

  try {
    const { data } = await (SSR.API.graphql({
      query: getBasicUserInformation,
      variables: {
        id,
      },
    }) as Promise<{ data: GetUserQuery }>);

    if (data.getUser.id) {
      const picture = data.getUser.picture
        // the method get does not exist
        ? ((await SSR.Storage.get(data.getUser.picture)) as string)
        : null;

      return {
        props: { ...data.getUser, picture },
      };
    }
  } catch (error) {
    console.error(error);
    return {
      props: {
        id: null,
      },
    };
  }
};

This is the error I get:

TypeError: Cannot read property 'get' of null
    at getServerSideProps (webpack-internal:///./src/pages/perfil/[id]/index.tsx:71:64)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async renderToHTML (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/render.js:39:215)
    at async /home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:99:97
    at async /home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:92:142
    at async DevServer.renderToHTMLWithComponents (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:124:387)
    at async DevServer.renderToHTML (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:125:874)
    at async DevServer.renderToHTML (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/server/next-dev-server.js:34:578)
    at async DevServer.render (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:72:236)
    at async Object.fn (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:56:618)
    at async Router.execute (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/router.js:23:67)
    at async DevServer.run (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:66:1042)
    at async DevServer.handleRequest (/home/andres/Entrepreneurship/TeVi/node_modules/next/dist/next-server/server/next-server.js:34:1081)

Expected behavior
Get the image from the getServerSideProps function

  • Device: Ubuntu 20
  • Browser Google chrome
  • Versions:
    "aws-amplify": "^3.3.13",
    "next": "^10.0.4",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
SSR Storage feature-request

Most helpful comment

There's good news & bad news to this.

The good news is that you can provide modules to withSSRContext to get new instances of categories per-request:

import { Amplify, API, Storage, withSSRContext } from 'aws-amplify'
import { GRAPHQL_AUTH_MODE, GraphQLResult } from '@aws-amplify/api-graphql'
import { NextApiRequest, NextApiResponse } from 'next'

import awsconfig from '../../src/aws-exports'
import { ListTodosQuery } from '../../src/API'
import { listTodos } from '../../src/graphql/queries'

Amplify.configure(awsconfig)

export default async (req: NextApiRequest, res: NextApiResponse) => {
  // ๐Ÿ‘‡  Notice how I'm explicitly created the SSR-specific instances of API & Storage
  const SSR = withSSRContext({ modules: [API, Storage], req })

  try {
    const result = (await SSR.API.graphql({
      // API has been configured with
      // GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS as the default authMode
      authMode: GRAPHQL_AUTH_MODE.AWS_IAM,
      query: listTodos,
    })) as GraphQLResult<ListTodosQuery>

    // ๐Ÿ‘‡  This will fail on the server     
    console.log(await SSR.Storage.get('foo'))

    return res.status(200).json({ data: result?.data?.listTodos?.items })
  } catch (error) {
    console.error(error)
    return res.status(500).json({ error })
  }
}

The bad news is that the Storage category uses AWSS3Provider, which does _not_ have access to request-specific credentials.


Note to self โ€“ proposal on how to fix this

The proposed changes would make Storage such that:

  • Credentials defaults to the single instance shared on the client (like it does today).
  • When Storage pluggables are configured, they receive a reference to those Credentials.
  • When withSSRContext is called, it creates a new instance of Storage & Credentials, and a new instance of AWSS3Provider as well.
  • These new Credentials will be populated with any cookie-based credentials from the client, and passed through the chain to AWSS3Provider.

[WARN] 16:11.124 AWSS3Provider - ensure credentials error No Cognito Identity pool provided for unauthenticated access
No credentials

This means there's more work for us to do to add proper Storage support for SSR:

  1. AWSS3Provider uses a single instance of Credentials, but should use a scoped instance (e.g. this.Credentials)

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/providers/AWSS3Provider.ts#L455-L457

  2. Even with this.Credentials used within AWSS3Provider, _they still need to be injected_. Storage first needs to declare Credentials as an instance variable.

    Other categories like Auth have Credentials injected into them because they declare an instance property:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/auth/src/Auth.ts#L91-L101

  3. AWSS3Provider is injected by default when no other pluggables are defined:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/Storage.ts#L155-L157

    When pluggables are configured, they can use also pass { Credentials = this.Credentials }:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/Storage.ts#L64

  4. Finally, AWSS3Provider can override this.Credentials based on the value in config:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/providers/AWSS3Provider.ts#L106-L115

All 5 comments

@MontoyaAndres Currently, Amplify only supports the API, Auth, & DataStore categories for SSR when using withSSRContext. However, I'm going to mark this as a feature request so it is accounted for in future SSR updates ๐Ÿ‘

https://github.com/aws-amplify/amplify-js/blob/b3761aea625e4e1c266d5170a6cdc247eaa54bba/packages/aws-amplify/src/withSSRContext.ts#L16-L17

For the time being, you'll likely need to defer the Storage.get() call to the client.

Thanks @amhinson is there a way to add Storage as a module? I'm seeing that the method withSSRContext has a parameter for adding modules...

There's good news & bad news to this.

The good news is that you can provide modules to withSSRContext to get new instances of categories per-request:

import { Amplify, API, Storage, withSSRContext } from 'aws-amplify'
import { GRAPHQL_AUTH_MODE, GraphQLResult } from '@aws-amplify/api-graphql'
import { NextApiRequest, NextApiResponse } from 'next'

import awsconfig from '../../src/aws-exports'
import { ListTodosQuery } from '../../src/API'
import { listTodos } from '../../src/graphql/queries'

Amplify.configure(awsconfig)

export default async (req: NextApiRequest, res: NextApiResponse) => {
  // ๐Ÿ‘‡  Notice how I'm explicitly created the SSR-specific instances of API & Storage
  const SSR = withSSRContext({ modules: [API, Storage], req })

  try {
    const result = (await SSR.API.graphql({
      // API has been configured with
      // GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS as the default authMode
      authMode: GRAPHQL_AUTH_MODE.AWS_IAM,
      query: listTodos,
    })) as GraphQLResult<ListTodosQuery>

    // ๐Ÿ‘‡  This will fail on the server     
    console.log(await SSR.Storage.get('foo'))

    return res.status(200).json({ data: result?.data?.listTodos?.items })
  } catch (error) {
    console.error(error)
    return res.status(500).json({ error })
  }
}

The bad news is that the Storage category uses AWSS3Provider, which does _not_ have access to request-specific credentials.


Note to self โ€“ proposal on how to fix this

The proposed changes would make Storage such that:

  • Credentials defaults to the single instance shared on the client (like it does today).
  • When Storage pluggables are configured, they receive a reference to those Credentials.
  • When withSSRContext is called, it creates a new instance of Storage & Credentials, and a new instance of AWSS3Provider as well.
  • These new Credentials will be populated with any cookie-based credentials from the client, and passed through the chain to AWSS3Provider.

[WARN] 16:11.124 AWSS3Provider - ensure credentials error No Cognito Identity pool provided for unauthenticated access
No credentials

This means there's more work for us to do to add proper Storage support for SSR:

  1. AWSS3Provider uses a single instance of Credentials, but should use a scoped instance (e.g. this.Credentials)

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/providers/AWSS3Provider.ts#L455-L457

  2. Even with this.Credentials used within AWSS3Provider, _they still need to be injected_. Storage first needs to declare Credentials as an instance variable.

    Other categories like Auth have Credentials injected into them because they declare an instance property:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/auth/src/Auth.ts#L91-L101

  3. AWSS3Provider is injected by default when no other pluggables are defined:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/Storage.ts#L155-L157

    When pluggables are configured, they can use also pass { Credentials = this.Credentials }:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/Storage.ts#L64

  4. Finally, AWSS3Provider can override this.Credentials based on the value in config:

    https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/providers/AWSS3Provider.ts#L106-L115

One way that I audited the codebase for all potential SSR "misses" was simply searching for \sCredentials.get().

If the call wasn't using this.Credentials.get(), then that means it's using a singleton which won't be request-specific on the server. ๐Ÿ”Ž

Thanks! If someone wants to do something similar, one quick and simple way is to just use Credentials from @aws-amplify/core and get the image using the aws-sdk, something like this (In my case I'm using the api from next.js/vercel):

import Amplify from 'aws-amplify';
import { Credentials } from '@aws-amplify/core';
import * as S3 from 'aws-sdk/clients/s3';
import { S3Customizations } from 'aws-sdk/lib/services/s3';
import { NowRequest, NowResponse } from '@vercel/node';

import awsconfig from 'aws-exports';

Amplify.configure({ ...awsconfig, ssr: true });

interface IParams {
  Bucket: string;
  Key: string;
}

function getImage(s3: S3Customizations, params: IParams) {
  return new Promise<string>((resolve, rejected) => {
    try {
      const url = s3.getSignedUrl('getObject', params);
      resolve(url);
    } catch (error) {
      console.error(error);
      rejected(error);
    }
  });
}

export default async (request: NowRequest, response: NowResponse) => {
  const { body } = request;

  try {
    const credentials = await Credentials.get();
    const s3 = new S3({
      apiVersion: '2006-03-01',
      params: { Bucket: awsconfig.aws_user_files_s3_bucket },
      signatureVersion: 'v4',
      region: awsconfig.aws_user_files_s3_bucket_region,
      credentials,
    });
    const picture = await getImage(s3, {
      Bucket: awsconfig.aws_user_files_s3_bucket,
      Key: (`public/${body.key}` as string) || '',
    });

    response.json({ picture });
  } catch (error) {
    console.error(error);
    response.json({ error });
  }
};

That's it, works fine for me, while this is resolved by the Amplify team :).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DougWoodCDS picture DougWoodCDS  ยท  3Comments

cgarvis picture cgarvis  ยท  3Comments

leantide picture leantide  ยท  3Comments

josoroma picture josoroma  ยท  3Comments

guanzo picture guanzo  ยท  3Comments