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
"aws-amplify": "^3.3.13",
"next": "^10.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.1",
@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 ๐
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.
The proposed changes would make Storage such that:
Credentials defaults to the single instance shared on the client (like it does today).Credentials.withSSRContext is called, it creates a new instance of Storage & Credentials, and a new instance of AWSS3Provider as well.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:
AWSS3Provider uses a single instance of Credentials, but should use a scoped instance (e.g. this.Credentials)
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:
AWSS3Provider is injected by default when no other pluggables are defined:
When pluggables are configured, they can use also pass { Credentials = this.Credentials }:
Finally, AWSS3Provider can override this.Credentials based on the value in config:
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 :).
Most helpful comment
There's good news & bad news to this.
The good news is that you can provide
modulestowithSSRContextto get new instances of categories per-request:The bad news is that the
Storagecategory usesAWSS3Provider, 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:
Credentialsdefaults to the single instance shared on the client (like it does today).Credentials.withSSRContextis called, it creates a new instance ofStorage&Credentials, and a new instance ofAWSS3Provideras well.Credentialswill be populated with any cookie-based credentials from the client, and passed through the chain toAWSS3Provider.This means there's more work for us to do to add proper
Storagesupport for SSR:AWSS3Provideruses a single instance ofCredentials, 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
Even with
this.Credentialsused withinAWSS3Provider, _they still need to be injected_.Storagefirst needs to declareCredentialsas an instance variable.Other categories like
AuthhaveCredentialsinjected into them because they declare an instance property:https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/auth/src/Auth.ts#L91-L101
AWSS3Provideris 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
Finally,
AWSS3Providercan overridethis.Credentialsbased on the value inconfig:https://github.com/aws-amplify/amplify-js/blob/e0789af92ad043bd4c069d766158942a5e6b2cae/packages/storage/src/providers/AWSS3Provider.ts#L106-L115