question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Method get of Storage is not supported for SSR

See original GitHub issue

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",

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:8
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

6reactions
ericclemmonscommented, Jan 6, 2021

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

3reactions
MontoyaAndrescommented, Jan 24, 2021

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 😃.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Next.js use localstorage problem with SSR - Stack Overflow
React Hook useEffect has missing dependencies: 'initialize' and 'key'. So when i add initialzie method to the depenedncy, it says that the ...
Read more >
SSR Support for AWS Amplify JavaScript Libraries
Enabling Server-Side Rendering (SSR) support in an Amplify app ... how to build SSR Next.js apps using Amplify using the new Next.js getting...
Read more >
Incremental Static Regeneration - Data Fetching - Next.js
Note: The experimental-edge runtime is currently not compatible with ISR, although can leverage stale-while-revalidate by setting the cache-control header ...
Read more >
Getting Started with Server-Side Rendering (SSR) - JavaScript
If your client-side code only reads from the server-side props and doesn't perform any updates to these models, then your client-side code won't...
Read more >
Using localStorage with React Hooks - LogRocket Blog
Working with a fresh React application, let's head over to the ... To see a list of the Storage methods, open the browser...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found