jamesmbourne / aws4-axios

Axios request interceptor for signing requests with AWSv4
MIT License
108 stars 39 forks source link

not compatible with axios 1.x #701

Closed zoli-kasa closed 1 year ago

zoli-kasa commented 2 years ago

using the example

const axios = require("axios");
const aws4Interceptor = require("aws4-axios").aws4Interceptor;

const interceptor = aws4Interceptor({
  region: "eu-west-2",
  service: "execute-api",
});

axios.interceptors.request.use(interceptor);

axios.get("https://example.com/foo").then((res) => {
  console.log(res);
});

with latest axios (1.1.2) i get the following error

node:internal/modules/cjs/loader:488
      throw e;
      ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/helpers/buildURL' is not defined by "exports" in /HIDDEN/iamtest/node_modules/axios/package.json
    at new NodeError (node:internal/errors:372:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:472:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:753:3)
    at resolveExports (node:internal/modules/cjs/loader:482:36)
    at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/HIDDEN/iamtest/node_modules/aws4-axios/dist/interceptor.js:55:34) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

with axios 0.27, it works fine.

jeremy-brooks commented 1 year ago

We have noticed the same exact thing, working for axios version 0.27.2 but broken for version 1.1.2

jeremy-brooks commented 1 year ago

Looking at the aws4-axios package.json, I see that this package is specifying "axios": ">=0.25.0" as a peer dependency.

What if, for now this was changed to "axios": ">=0.25.0 < 1"?

Then when axios fixes it's issue, this can be updated to >=1.0.0 or similar?

nguyentoanit commented 1 year ago

Same here. 👍

ivantm commented 1 year ago

Still broken with axios 1.2.1.

npm is also partly to blame here as the resolutions in package.json should allow 0.2x to be installed but attempting to override it leads to invalid: "0.27.2" from node_modules ....

In addition to specifying "axios": ">=0.25.0 <1:" in the overrides field in package.json, deleting node_modules and package-lock.json is required as per https://github.com/npm/cli/issues/4232

florianbepunkt commented 1 year ago

The axios team stated in multiple issues that they will not export helper functions in the foreseeable future. In my opinion, a path forward would be to replicate the used lib utility functions in the aws-axios codebase, as they are not overly complicated / difficult to maintain. @jamesmbourne What do you think? Would you be open to a pr?

kesavab commented 1 year ago

We are facing the same issue too, works with 0.27.2

florianbepunkt commented 1 year ago

@jamesmbourne Could you have a look at my proposal https://github.com/jamesmbourne/aws4-axios/issues/701#issuecomment-1353024764 ? Would you be open to a PR?

jasonwadsworth commented 1 year ago

@jamesmbourne Could you have a look at my proposal https://github.com/jamesmbourne/aws4-axios/issues/701#issuecomment-1353024764 ? Would you be open to a PR?

I'm tempted to fork this just to be able to address this issue.

zoli-kasa commented 1 year ago

@jamesmbourne Could you have a look at my proposal #701 (comment) ? Would you be open to a PR?

I'm tempted to fork this just to be able to address this issue.

that's great, go for it if you have time! the package looks kind of abandoned :(

jamesmbourne commented 1 year ago

Hi all, sorry I haven't had the time to put any effort into maintain this package over the past year.

I'm not currently using this package for any of my projects (I've switched to https://github.com/sindresorhus/got for most things).

That said, it's clear that this package is still depended upon by a lot of projects, so I'll try and at least get it to a state where it's compatible with axios>=1.0.0.

If anyone has forked the project to add support for this, I'd be very welcoming of PRs and will endeavour to get those merged and released ASAP.

jamesmbourne commented 1 year ago

I've taken a look at this and currently I think we would need to include half of Axios's helpers dir in this repo. The most complex function used is https://github.com/axios/axios/blob/v1.x/lib/helpers/buildURL.js due to a number of other dependencies.

If I went ahead and copied these implementations into this repo, there is a high likelihood of something breaking due to subtle differences in how the "copy" of Axios in this repo behaves compared with the real version.

Please could you give a 👍 on this issue: https://github.com/axios/axios/issues/4793

Having the final request URL available in interceptors seems like the only sensible way to go about achieving compatibility with Axios 1.x to me.

florianbepunkt commented 1 year ago

@jamesmbourne Is it only about the final url? If so, this should work:

const client = axios.create({
  baseURL: "https://some-domain.com/api/",
  params: { foo: "bar" },
});

client.interceptors.request.use(async (config) => {
  console.log("config", config);

  const uri = client.getUri(config);
  console.log("final uri", uri); // final uri https://some-domain.com/api/foooooo?foo=bar
  return config;
});

try {
  const a = await client.get("/foooooo");
} catch (error) {
  console.log(error);
}

Just a quick demo:

import { fromIni } from "@aws-sdk/credential-providers";
import aws4 from "aws4";
import axios from "axios"; // current axios version
import type { AwsCredentialIdentity, Provider } from "@aws-sdk/types";
import type {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosRequestHeaders,
  InternalAxiosRequestConfig,
  Method,
} from "axios";

const getTransformer = (config: AxiosRequestConfig) => {
  const { transformRequest } = config;

  if (transformRequest) {
    if (typeof transformRequest === "function") {
      return transformRequest;
    } else if (transformRequest.length) {
      return transformRequest[0];
    }
  }

  throw new Error("Could not get default transformRequest function from Axios defaults");
};

const client = axios.create({
  baseURL: "https://some-url.com",
});

export const aws4Interceptor =
  ({
    instance = axios,
    credentials,
    options,
  }: {
    instance: AxiosInstance;
    options?: InterceptorOptions;
    credentials: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;
  }): ((config: InternalAxiosRequestConfig) => Promise<InternalAxiosRequestConfig>) =>
  async (config): Promise<InternalAxiosRequestConfig> => {
    const url = instance.getUri(config);
    console.log("final url", url);

    const { host, pathname, search } = new URL(url);
    const { data, headers, method } = config;
    const transformRequest = getTransformer(config);
    // @ts-ignore
    const transformedData = transformRequest(data, headers);

    // Remove all the default Axios headers
    const {
      common,
      delete: _delete, // 'delete' is a reserved word
      get,
      head,
      post,
      put,
      patch,
      ...headersToSign
    } = headers as any as InternalAxiosHeaders;
    // Axios type definitions do not match the real shape of this object

    const signingOptions: SigningOptions = {
      method: method && method.toUpperCase(),
      host,
      path: pathname + search,
      region: options?.region,
      service: options?.service,
      signQuery: options?.signQuery,
      body: transformedData,
      headers: headersToSign as any,
    };

    const resolvedCredentials =
      typeof credentials === "function"
        ? await credentials()
        : credentials ?? {
            accessKeyId: "",
            secretAccessKey: "",
          };

    // using aws4
    aws4.sign(signingOptions as any, resolvedCredentials); // TODO typing
    config.headers = signingOptions.headers as any; // TODO typing

    if (signingOptions.signQuery) {
      const originalUrl = new URL(url);
      const signedUrl = new URL(originalUrl.origin + signingOptions.path);
      config.url = signedUrl.toString();
    }

    return config;
  };

const interceptor = aws4Interceptor({
  instance: client,
  credentials: fromIni({ profile: "some-profile" }),
  options: {
    region: "eu-central-1",
    service: "execute-api",
  },
});
client.interceptors.request.use(interceptor);

try {
  const a = await client.get("/games", { params: { foo: "bar" } });
  console.log("request succeed");
} catch (error) {
  console.log("req failed", error);
}

export type InternalAxiosHeaders = Record<Method | "common", Record<string, string>>;

export interface SigningOptions {
  host?: string;
  headers?: AxiosRequestHeaders;
  path?: string;
  body?: unknown;
  region?: string;
  service?: string;
  signQuery?: boolean;
  method?: string;
}

export interface Credentials {
  accessKeyId: string;
  secretAccessKey: string;
  sessionToken?: string;
}

export type InterceptorOptions = {
  /**
   * Target service. Will use default aws4 behavior if not given.
   */
  service?: string;
  /**
   * AWS region name. Will use default aws4 behavior if not given.
   */
  region?: string;
  /**
   * Whether to sign query instead of adding Authorization header. Default to false.
   */
  signQuery?: boolean;
  /**
   * ARN of the IAM Role to be assumed to get the credentials from.
   * The credentials will be cached and automatically refreshed as needed.
   * Will not be used if credentials are provided.
   */
  assumeRoleArn?: string;
  /**
   * Number of seconds before the assumed Role expiration
   * to invalidate the cache.
   * Used only if assumeRoleArn is provided.
   */
  assumedRoleExpirationMarginSec?: number;
};
tusbar commented 1 year ago

@jamesmbourne the axios internals are now exposed again: https://github.com/axios/axios/pull/5677, released as of 1.4.0.

jamesmbourne commented 1 year ago

Great news, thanks for sharing @tusbar! I’ll take a look at getting a PR ready using the newly exposed helpers.

florianbepunkt commented 1 year ago

@jamesmbourne As shown above, it might not be necessary to rely on the internal helpers. Axios maintainers marked them as unsafe and announced that they plan to refactor them. I am using the PoC above https://github.com/jamesmbourne/aws4-axios/issues/701#issuecomment-1484136280 for signing requests to an apigw and it works fine.

zoli-kasa commented 1 year ago

@jamesmbourne As shown above, it might not be necessary to rely on the internal helpers. Axios maintainers marked them as unsafe and announced that they plan to refactor them. I am using the PoC above #701 (comment) for signing requests to an apigw and it works fine.

@florianbepunkt aren't you planning to release that as a standalone NPM package?

florianbepunkt commented 1 year ago

@zoli-kasa Haven't thought about it. I prefer contributing to existing solutions. Publishing another package would add to (unnecessarily) fragmenting the solution space. I'm not against using the internal helpers and I have used this library extensively in the past. I just wondered whether the task at hand can be accomplished with the public API, which might make maintenance of this lib easier in the future.

jamesmbourne commented 1 year ago

Thanks for the suggestions all, especially @florianbepunkt!

client.getUri works nicely. I've pushed to https://github.com/jamesmbourne/aws4-axios/pull/835 and all the tests were passing.

I just need to update the docs and then we should be able to get a new v3 release out.

github-actions[bot] commented 1 year ago

:tada: This issue has been resolved in version 3.0.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Bingjiling commented 10 months ago

We just upgraded to Axios 1.6.0 and found that aws4-axios is not compatible. Could you please look into it? Thanks!