Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

APIサーバでのデータの送信方式の整理

はじめに

APIサーバを設計する際に、データの送信方法、特にファイルの送信方式で悩むことがある。 ここでは、HTTPのPOSTでデータを送信する時のパターンを整理してみた。

送りたいデータ: なし

言うまでもないが、この場合は単純にPOSTのリクエストを送信すればよい。 curlの実行例は以下の通りで、明示的にオプション-Xまたは--requestでPOSTであることを指定して実行する。

$ curl \
      --request POST \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*

送りたいデータ: JSON

普通にAPIを設計するならば、JSONの送受信となる。 curlコマンドの実行例を次に示す。

$ curl \
      --header "Content-Type: application/json" \
      --data '{"name":"Jaybanuan", "age":10, "favorite": "チョコ棒"}' \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 58

{"name":"Jaybanuan", "age":10, "favorite": "チョコ棒"}

送りたいデータ: YAML

JSONの場合とおおよそ同じやり方で送信できると思うが、メディアタイプapplication/yamlがまだ正式に登録されていない。 なのでContent-Typeは、ドラフトであることを承知でapplication/yamlを使うか、適当にアプリ固有のものを定義して使うことになる。

redj.hatenablog.com

YAMLJSONと互換があるので、現時点ではYAMLを直接転送するよりは、YAMLJSONに変換して、それをContent-Type: application/jsonで送信するのが無難かと思う。

送りたいデータ: Key-Value

送りたいデータが単純なKey-Valueの場合は、HTTPの仕様に頼って送るのも一つの手である。 メリットは、クライアント側としてはcurlコマンド等で送りやすいことと、サーバ側としてはWebアプリのフレームワークがある程度処理を肩代わりしてくれること。 デメリットは、利用者が通信プロトコル(HTTP)をある程度理解する必要があること。

(1) Content-Type: application/x-www-form-urlencodedを利用

この場合は、Key-Valueの個々のデータを&で連結したものが、HTTPボディとして送信される。 curlコマンドでは、以下のオプションを利用することでKey-Valueのデータを送付できる。

  • -d または --data
  • --data-urlencode
  • --data-raw

これらのオプションの利用時は、自動的に以下が設定される。

  • HTTPメソッドはPOSTに設定
  • HTTPヘッダのContent-Typeapplication/x-www-form-urlencodedに設定

curlコマンドの実行例を次に示す。

$ curl \
      --data name=jaybanuan \
      --data-raw age=10 \
      --data-urlencode "favorite=チョコ棒" \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 67
Content-Type: application/x-www-form-urlencoded

name=jaybanuan&age=10&favorite=%E3%83%81%E3%83%A7%E3%82%B3%E6%A3%92

(2) Content-Type: multipart/form-dataを利用

この場合は、Key-Valueの個々のデータは、マルチパートとして送信される。 curlコマンドでは、以下のオプションを利用することでKey-Valueのデータを送付できる。

  • -F または --form

これらのオプションの利用時は、自動的に以下が設定される。

  • HTTPメソッドはPOSTに設定
  • HTTPヘッダのContent-Typemultipart/form-dataに設定

curlコマンドの実行例を次に示す。

$ curl \
      --form name=jaybanuan \
      --form age=10 \
      --form "favorite=チョコ棒" \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 351
Content-Type: multipart/form-data; boundary=------------------------e297819c8d0c28ba

--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="name"

jaybanuan
--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="age"

10
--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="favorite"

チョコ棒
--------------------------e297819c8d0c28ba--

application/x-www-form-urlencodedの場合と比べるとHTTPボディが重くなっている。 しかし、Key-Valueの送信に加えて、後述のようにファイルも送信する場合は、こちらを利用する必要がある。

送りたいデータ: ファイル

ここで、以下の内容の2つのファイルをサーバに送りたいとする。 1つ目はgreeting.txtというファイル名のテキストファイルで、内容は次のとおり。

Hello, World!

2つ目はgreeting.jsonというファイル名のJSONで、内容は次のとおり。

{
    "greeting": "Hello, World!"
}

(1) Content-Type: multipart/form-dataを利用

HTTPのマルチパートにより、ファイルを転送する。 HTTPでファイル送信を行う際の一般的な方法であり、広く使われている。 curlコマンドの実行例を次に示す。

$ curl \
      --form greeting-en=@greeting.txt \
      --form "greeting-json=@greeting.json;type=application/json" \
      http://localhost:8080/

各パートのContent-Typeを指定したい場合は、ファイル名の後ろにtypeで指定する。 また、各パートにその他のヘッダを付与したい場合はheadersで指定する。

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 405
Content-Type: multipart/form-data; boundary=------------------------7aa5ee307312820c

--------------------------7aa5ee307312820c
Content-Disposition: form-data; name="greeting-en"; filename="greeting.txt"
Content-Type: text/plain

Hello, World!
--------------------------7aa5ee307312820c
Content-Disposition: form-data; name="greeting-json"; filename="greeting.json"
Content-Type: application/json

{
    "greeting": "Hello, World!"
}
--------------------------7aa5ee307312820c--

この方式のメリットは、大抵のHTTPクライアントツールやWebアプリのフレームワークはマルチパートをサポートしているため、それを活用することで自力での実装部分を減らせること。 一方でデメリットは、HTTPの都合が色濃く出るので、JSONベースのAPIでのファイルアップロードに利用すると、統一感が出ないこと。

(2) ファイルをJSONに詰め込んで送信

ファイル送信用のJSONベースのAPIを設計して、ファイル名やファイルの内容をJSONに詰め込んで送信する。 以下は簡素なJSONベースのファイル送信APIの例。 この例では、ファイルの内容はJSONの文字列として扱えるようにBASE64エンコードしている。

[
    {
        "filename": "greeting.txt",
        "content": "SGVsbG8sIFdvcmxkIQ=="
    },
    {
        "filename": "greeting.json",
        "content": "ewogICAgImdyZWV0aW5nIjogIkhlbGxvLCBXb3JsZCEiCn0="
    }
]

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 204

[    {        "filename": "greeting.txt",        "content": "SGVsbG8sIFdvcmxkIQ=="    },    {        "filename": "greeting.json",        "content": "ewogICAgImdyZWV0aW5nIjogIkhlbGxvLCBXb3JsZCEiCn0="    }]

上記のcurlの実行例では、オプション--dataを利用しているので、JSONから改行がなくなっている。 JSONの場合は改行に依存しないので問題ないが、余計な変換をしたくない場合はオプション--data-rawを利用すればよい。

マルチパートの時の裏返しになるが、この方式のメリットは、JSONベースのAPIでの実装になり統一感が出ること。 一方でデメリットは、サーバ側ではAPIの設計から実装まで自力で行う必要があることと、クライアント側ではJSONの組み立てが面倒でcurlコマンドで手軽にファイル送信ができないこと。

参考

weblabo.oscasierra.net

YAMLのMedia Type (application/yaml)

APIを設計していて、YAMLのMedia Type (application/yaml)って見たことないけどあったっけ?と思って調べてみた。 2022年8月時点では、YAMLはまだIETFのMedia Typeの一覧に載っていない。

どうもイタリア政府のDigital Transformation Departmentという機関がYAMLのMedia Typeを登録しようとしているようで、現時点ではまだドラフト。 ぜひ登録してほしい。

Nginx Unitのコンテナのdocker-entrypoint.sh

Nginx Unitをベースとしたコンテナイメージを作る際に、/docker-entrypoint.dというドロップインディレクトリにNginx Unitの設定ファイルを配置する。

unit.nginx.org

ただ、詳細な説明がないので、挙動の確認はドキュメント読むよりもコード読んだほうが早い。 以下にNginx Unitのdocker-entrypoint.shのリンクを貼っておく。

github.com

ContentType: application/x-www-form-urlencodedの使い道

curlでPOSTのbodyを送るときに、オプションの付与の仕方が何種類かあって、その中でURLエンコードってなんだっけとなって悩んだ。 はるか昔に調べて納得した記憶はあるが、詳細は忘れてしまった。 ズバリの回答が以下にある。

teratail.com

当該部分を引用しておく。

さてではコンテントタイプがapplication/x-www-form-urlencodedのときにURLエンコードが必要となる理由。これはkey=value形式のデータを&区切りで並べる以上、value部に'&'が入っているとvalueの一部なのか区切り文字なのか見分けがつかないからエスケープする必要があるという、HTTPよりは高レベルのアプリケーション上の要求でとなります。

つまり、HTMLのformをうまく取り扱うためのContentTypeということが分かる。 逆に言うと、form以外では使い道はないと思う。

純粋にPOSTでデータを送りたいときは、

  • 単純にbodyにデータを突っ込んでデータに合わせたContentType (JSONならapplication/json)を設定するか、
  • マルチパートを使う

ことになるはず。

URIにおけるQueryとSearchは同じもの(だと思う)

ちょっとだけJavaScriptを勉強中。

URIのクエリ(query)とは、URIの文字列中で?の後に続くキーバリューの値で、RFC 3986では以下のように説明されている。

   The following are two example URIs and their component parts:

         foo://example.com:8042/over/there?name=ferret#nose
         \_/   \______________/\_________/ \_________/ \__/
          |           |            |            |        |
       scheme     authority       path        query   fragment
          |   _____________________|__
         / \ /                        \
         urn:example:animal:ferret:nose

JavaScriptでクエリを処理する場合は、location.searchで取得したり、URLSearchParamsで分解したりするようだ。 しかし、searchという単語が聞き慣れず不安になったので調べてみた。

正確な情報は見つからなかったが、どうもqueryとsearchは同じものを指しているようで、表記ゆれっぽい。

www.robinwieruch.de

Visual Studio CodeにPythonのコードのパスを認識させる

はじめに

諸般の事情で、Gitのリポジトリの少し奥の方にあるPythonのコードを開発することになった。 例えば、以下のような感じ。

python-path-on-vscode
`-- calc
    |-- src
    |   `-- calc.py
    `-- tests
        `-- test_calc.py

Visual Studio Codeで開発するのだが、Pythonコードのパスについての環境変数PYTHONPATHを設定しておかないと不便なので、調べたことをメモしておく。

Python Extension用の設定

Microsoftが提供しているPython Extensionに環境変数PYTHONPATHを認識させるためには、Gitのリポジトリに以下の内容で.envを作成する。

PYTHONPATH=calc/src

この設定をしていないと、Python Extensionがコードの位置を認識できず、関数の定義位置にジャンプができないなどの不便が生じる。

Terminal用の設定

Visual Studio CodeのTerminalで環境変数PYTHONPATHを有効にするには、Gitのリポジトリに以下の内容で.vscode/settings.jsonを作成する。

{
    "terminal.integrated.env.linux": {
        "PYTHONPATH": "${workspaceFolder}/calc/src"
    }
}

この設定をしていないと、Terminalから実行したpytestがコードの位置を認識できず、テストコードのimport文でエラーが起きるなどの不便が生じる。

最終的なファイル構成

python-path-on-vscode
|-- .env
|-- .vscode
|   `-- settings.json
`-- calc
    |-- src
    |   `-- calc.py
    `-- tests
        `-- test_calc.py

参考

code.visualstudio.com

pymongoでのコネクションプールについて

PythonのWebアプリ(Flask)からMongoDBを利用する際に、コネクションプールはどう扱ったらいいんだろうと思って調べた。 すると、MongoDBに接続するドライバpymongoそのものに、コネクションプールの機能があることが分かった。 以下のpymongoのFAQを参照。

pymongo.readthedocs.io