---
title: "Making Cloudfront Signed URLs work with response-content-disposition"
description: "A workaround for the Node.js AWS SDK to make Cloudfront Signed URLs work with response-content-disposition."
datePublished: 2022-11-28T18:39:00.000Z
slug: "cloudfront-signed-disposition"
previewImage: "/preview/cloudfront-disposition.png"
permalink: "/blog/cloudfront-signed-disposition"
---
As of `v3.219.0` of the `@aws-sdk/cloudfront-signer` package, adding a `response-content-disposition` query parameter to the URL being sent to the `getSignedUrl` function will result in an `Access Denied` error on the output Cloudfront URL:

```js
const disposition = 'attachment;filename="test.txt"';

const signedUrl = getSignedUrl({
	keyPairId: config.cloudfrontKeyPairId,
	privateKey: config.cloudfrontSigningKey,
	url: `https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=${encodeURIComponent(
		contentDisposition
	)}"`,
	dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});

console.log(signedUrl);
// https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=attachment;filename=newFilename.txt&Policy=XXXXXX&Key-Pair-Id=YYYYYY&Signature=ZZZZZZ
```

The above code will result in an `Access Denied` error when the URL is accessed:

```xml
<Error>
  <Code>AccessDenied</Code>
  <Message>Access denied</Message>
</Error>
```

Evidently, the issue happens when the encoded URL (using `encodeURIComponent`) is reverted by the `getSignedUrl` function.

From the source, it seems that the input `url` parameter is fed into the [`@aws-sdk/url-parser`](https://github.com/aws/aws-sdk-js-v3/blob/0a20fe50fd2bace16837c88a7d7e0fe57e329363/packages/url-parser/src/index.ts) package, which is presumably where the encoded URL is being lost in translation.

```ts
import { parseUrl } from "@aws-sdk/url-parser";

...

/**
 * Creates a signed URL string using a canned or custom policy.
 * @returns the input URL with signature attached as query parameters.
 */
export function getSignedUrl({
  dateLessThan,
  dateGreaterThan,
  url,
  keyPairId,
  privateKey,
  ipAddress,
  policy,
}: CloudfrontSignInput): string {
  const parsedUrl = parseUrl(url);
	...
}
```

## The Workaround

The [workaround](https://github.com/aws/aws-sdk-js-v3/issues/3614#issuecomment-1129196076) is to manually set the query parameter after parsing the signed URL:

```js
const disposition = `attachment;filename=newFilename.txt;`;

const url = new URL(`test.txt`, `https://d1x2y3z4.cloudfront.net`);
url.searchParams.append('response-content-disposition', disposition);

const signedUrl = getSignedUrl({
	url: url.href,
	keyPairId: config.cloudfrontKeyPairId,
	privateKey: config.cloudfrontSigningKey,
	dateLessThan: expires.toISOString(),
	dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});

const fixedSignedUrl = new URL(signedUrl);
fixedSignedUrl.searchParams.set('response-content-disposition', disposition);
return fixedSignedUrl.href;
```

Which works out just fine! 🎉

From the comments on [this](https://github.com/aws/aws-sdk-js-v3/issues/3614) GitHub issue, a "fix" doesn't seem to be in the works. I ended up losing many a few hours trying to figure out what was going on. Hoping that this helps out someone else.