[小ネタ] curlコマンドでクエリ文字列を指定してAPIを叩く時に試行錯誤したこと

2023.03.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部Delivery部のアベシです。

筆者はAPIのテストにいつもPOSTMANTHUNDER CLIENTを使ってました。
そのため、いざcurlコマンドでAPIの検証してみたらいくつかハマったので、解消するまでの試行錯誤と結局どう書くのが良さそうと思ったのかを記事に残したいと思います。

エラー再現とレスポンス確認のため、以下のコードでfugapiyoの2つのクエリ文字列を取るAPIを作成しました。

このようなAPIにクエリ文字列を指定してGETメソッドでリクエストを送りパラメーターを渡す、というのがやりたかったことです。

CDKのコード

import {
  Stack,
  StackProps,
  aws_apigateway,
  aws_lambda_nodejs,
} from 'aws-cdk-lib';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';


export class ApiSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // Lambdaのビルド
    const nameFunc = "Lambda_invoke_sample"
    const func = new aws_lambda_nodejs.NodejsFunction(
      this,
      nameFunc,
      {
        runtime: Runtime.NODEJS_18_X,
        functionName: nameFunc,
        entry: 'src/lambda/handlers/handler.ts',
      },
    );

    // API Gatewayリソースの作成
    const nameRestApi ="API-sample";
    const restApi = new aws_apigateway.RestApi(this, nameRestApi, {
      restApiName: nameRestApi,
      deployOptions: {
        stageName: 'v1',
      },
    });

    const resource= restApi.root.addResource('hoge');

    resource.addMethod(
      'GET',
      new aws_apigateway.LambdaIntegration(func),
      //クエリ文字列の指定
      {
        requestParameters:{
          "method.request.querystring.fuga": true,
          "method.request.querystring.piyo": true,
        }
      }
    );
  }
}

Lambda関数のコード

ハンドラーは受け取ったeventをクライアントに返します。 このeventの中に受信したクエリ文字列も入ってます。

interface Response {
  statusCode: number;
  body: string;
}

export const handler = async (event: Event): Promise<Response> => {
    return {statusCode: 200 , body:JSON.stringify(event, undefined, 2)};
};


-dオプションでクエリ文字列を指定しようと頑張る

curlには色々なオプションが用意されていて、-dオプションを使って複数のクエリ文字列を送っている記事を目にしたのでこのオプションでやってみました

-dオプションのみ指定

以下のコマンドで叩きます

$ curl -i -d fuga=1 -d piyo=1 https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge

クエリ結果

HTTP/2 403
~中略~

{"message":"Missing Authentication Token"}

"Missing Authentication Token"が返って来ました。
このエラー、AWS公式のknowledge-centerの情報に以下の記述が有りました。

次の理由により、API Gateway REST API エンドポイントは「Missing Authentication Token」(認証トークンが見つかりません) エラーを返します。

API リクエストが、存在しないメソッドまたはリソースに対して行われた。

リクエスト先のURLとリソースは問題なさそうなのでメソッドに目をつけました。 curlコマンドではメソッドを指定しない場合はGETメソッドでリクエストするという記事を目にしたのでメソッド指定無しでリクエストしましたが、ここが怪しそうなので明示的にメソッドを指定して再チャレンジしました。

-Xオプションを追加

調べると-Xオプションで指定できそだったのでGETメソッドを指定してたたいてみました。

コマンド

$ curl -i -X GET -d fuga=1 -d piyo=1 https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge

結果

HTTP/2 403
server: CloudFront
date: Thu, 09 Mar 2023 08:34:39 GMT
content-type: text/html
content-length: 915
x-cache: Error from cloudfront
~以下略~

結果はこれもうまく行かずでAPI GatewayのURLにリクエストしているのもかかわらず、CloudFrontからエラーが返ってきました。 この現象の発生理由はわかりませんでした。

-X を -Gオプションに変更

次に試したのが -Gというオプションです。
これはGETメソッドを強制するものとのことで試してみました。

コマンド

$ curl -i -G -d fuga=1 -d piyo=1 https:// **********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge

結果

HTTP/2 200
~中略~
  "queryStringParameters": {
    "fuga": "1",
    "piyo": "1"
  },
~以下略~
}

やっとリクエストが成功し、Lambda関数が受けたeventが返ってきました。
パラメーターもちゃんと受信できてました。

-dオプションはPOSTのリクエストボディにデータを含めるために使用するものだった

この-dオプションはcontent-typeapplication/x-www-form-urlencodedのPOSTのリクエストボディのデータを指定するためのものでした 。
curlの公式に書かれてました。
今回の場合やりたいことはURLのパラメーターとしてクエリ文字列を指定したいので用途が間違ってました 。

helpの内容

-d, --data <data>        HTTP POST data
    --data-ascii <data>  HTTP POST ASCII data
    --data-binary <data> HTTP POST binary data
    --data-raw <data>    HTTP POST data, '@' allowed
    --data-urlencode <data> HTTP POST data URL encode

curlコマンドでは以下記述で全てのhelp要素を表示できます。

$ curl --help all

こちらにいままで試したオプションについてしっかり説明されておりました。
最初からこちらを見てオプションを試していればよかったです。
なにか詰まったらまずはhelpや公式情報を見るのが一番手っ取り早いですね。

-dオプションを使わない方法

-dオプションを使わない方法を調べると以下の形で指定するのが一般的とのことでした。

curl url?<パラメーター名>=<値>

//パラメーターが複数ある場合は&で続ける
curl url?<パラメーター名>=<値>&?<パラメーター名>=<値>

ということで上記の指定のしかたでもう一度試してみました。

コマンド

curl -i https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge/?fuga=1&piyo=1

結果

zsh: no matches found: https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge/?fuga=1

今度はzshシェルがエラーを吐きました。
ファイルを実行しようとして見つからなかった?場合のようなエラーです。

Z shellがエラーを吐く原因

こちら調べると、Z Shell特有のエラーらしくBashでは発生しないようです。 zshではコマンドに *,[],? などのglobパターンマッチ記号が含まれているとファイルとして解析しようとするようで、今回URLのクエリ文字列の指定に記載した?の所為でファイルと認識されたようです。

setopt nonomatch を .zshrc に追記

これを回避する方法としてsetopt nonomatchを.zshrcに記述する方法が有りました。
.zshrcに記載してsource ~/.zshrcしてからもう一度先程のcurlコマンドでAPIコールしてみましょう。

結果

HTTP/2 200
~中略~
  "queryStringParameters": {
    "fuga": "1"
  },

今度は問題なく200とeventが返ってきました。
しかし、、、受け取ったパラメーターをよくよく見るとfugaしか返ってきません。 なんでや。。。

クエリ文字列を一つしか受信できないのはなぜ?

またまた調べると&の後の記述がエンドポイントとして認識されておらずこのような現象が発生していることがわかりました。
curl公式には、エンドポイントのURLにシェルの制御に使う文字列( & ,? ,* などなど)を含む場合はシェルの干渉を防ぐためにURLはダブルクオートで囲みましょうと記載がありました。

ということでダブルクオートでURL全体を囲って叩いてみましょう

コマンド

curl -i "https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hoge/?fuga=1&piyo=1"

結果

HTTP/2 200
~中略~
  "queryStringParameters": {
    "fuga": "1",
    "piyo": "1"
  },

ついにいい感じにリクエスト成功しました。クエリ文字列もpiyoも含め全て返ってきました。

ダブルクオートよりもシングルクオートにすべき

このブログを公開してから先輩社員からダブルクオートでは変数展開の余地があり$が含まれたURLにコールできなくなる可能性について教えてもらいました。

変数展開とは

例えば、以下のようなコマンドで$API_KEYや$QUERYのような環境変数を参照してしまいます。URLに$が含まれているとこのように変数を参照してしまい、意図せずにその値を含むURLとなってしまう、とい事が考えられます。

curl "https://example.com/api?key=$API_KEY&query=$QUERY"

しかしシングルクオートであればこの変数展開も発生することがありません。
よってダブルクオートは使用せずシングルにするというのが一番良いというのが結論となります。

結論

まずクエリ文字列の指定が必要なGETリクエストの場合、URLに続けてパラメーターを記載します。
パラメーターが複数ある場合にきちんと認識するためと、変数展開を防ぐためにURL全体をシングルクオートで囲うのが一番シンプル且つ間違いがなさそうです!!

以上