Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/lucky-horses-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@opennextjs/aws": patch
---

add: s3 lite override for loading images in the image optimization server

`s3-lite` override for image loading. Uses `aws4fetch` to get the objects from your s3 bucket. This will make the image optimization server work without the aws s3 sdk. This override introduces a new environment variable called `BUCKET_REGION`. It will fallback to `AWS_REGION` ?? `AWS_DEFAULT_REGION` if undefined. This will require no additional change in IAC for most users.

```ts
import type { OpenNextConfig } from '@opennextjs/aws/types/open-next';
const config = {
default: {},
imageOptimization: {
loader: 's3-lite',
},
} satisfies OpenNextConfig;

export default config;
```
81 changes: 81 additions & 0 deletions packages/open-next/src/overrides/imageLoader/s3-lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Readable } from "node:stream";
import type { ReadableStream } from "node:stream/web";
import { AwsClient } from "aws4fetch";

import type { ImageLoader } from "types/overrides";
import { FatalError } from "utils/error";

let awsClient: AwsClient | null = null;

const { BUCKET_NAME, BUCKET_KEY_PREFIX } = process.env;
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
// If AWS_REGION is defined it will override AWS_DEFAULT_REGION
const BUCKET_REGION =
process.env.BUCKET_REGION ??
process.env.AWS_REGION ??
process.env.AWS_DEFAULT_REGION;

const getAwsClient = () => {
if (awsClient) {
return awsClient;
}
awsClient = new AwsClient({
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
sessionToken: process.env.AWS_SESSION_TOKEN,
region: BUCKET_REGION,
});
return awsClient;
};

function ensureEnvExists() {
if (!(BUCKET_NAME || BUCKET_KEY_PREFIX || BUCKET_REGION)) {
throw new FatalError("Bucket name, region and key prefix must be defined!");
}
}

const awsFetch = async (key: string, options: RequestInit) => {
const client = getAwsClient();
const url = `https://${BUCKET_NAME}.s3.${BUCKET_REGION}.amazonaws.com/${key}`;
return client.fetch(url, options);
};

const s3Loader: ImageLoader = {
name: "s3-lite",
load: async (key: string) => {
ensureEnvExists();
const keyPrefix = BUCKET_KEY_PREFIX?.replace(/^\/|\/$/g, "");
const response = await awsFetch(
keyPrefix
? `${keyPrefix}/${key.replace(/^\//, "")}`
: key.replace(/^\//, ""),
{
method: "GET",
},
);

if (response.status === 404) {
throw new FatalError("The specified key does not exist.");
}
if (response.status !== 200) {
throw new FatalError(
`Failed to get image. Status code: ${response.status}`,
);
}

if (!response.body) {
throw new FatalError("No body in aws4fetch s3 response");
}

// We need to cast it else there will be a TypeError: o.pipe is not a function
const body = Readable.fromWeb(response.body as ReadableStream<Uint8Array>);

return {
body: body,
contentType: response.headers.get("content-type") ?? undefined,
cacheControl: response.headers.get("cache-control") ?? undefined,
};
},
};

export default s3Loader;
7 changes: 6 additions & 1 deletion packages/open-next/src/types/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export type IncludedTagCache =
| "fs-dev"
| "dummy";

export type IncludedImageLoader = "s3" | "host" | "fs-dev" | "dummy";
export type IncludedImageLoader =
| "s3"
| "s3-lite"
| "host"
| "fs-dev"
| "dummy";

export type IncludedOriginResolver = "pattern-env" | "dummy";

Expand Down
Loading