HTTPアクション
httpアクションはHTTPリクエストを実行し、レスポンスをアサーションやoutputsから参照できるようにします。
基本的な構文
HTTPのステップにはURLとメソッドを指定し、レスポンスをtestで検証します。
steps:
- name: Check the API
uses: http
with:
url: "https://api.example.com/health"
method: GET
test: res.code == 200パラメータ
以下のフィールドでリクエストを指定します。いずれもテンプレート式を書けます。
| パラメータ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
url | String | 必須 | - | リクエストURL。メソッド省略記法でパスを渡す場合はベースURL |
method | String | 必須 | - | HTTPメソッド。メソッド省略記法を使う場合はそちらが設定します |
headers | Object | 任意 | - | リクエストヘッダー |
body | StringまたはObject | 任意 | - | リクエストボディ。content-typeがJSONの型のとき、オブジェクトはJSONにシリアライズされます。JSONの型とは、charsetなどのパラメータの有無を問わないapplication/jsonと、+jsonで終わる型です |
timeout | Duration | 任意 | 30s | レスポンスの読み取りまで含めた、リクエスト全体の制限時間 |
basic_auth | Object | 任意 | - | HTTP Basic認証のusernameとpassword。Authorizationヘッダーとして送ります |
form | Object | 任意 | - | フォームのフィールド。application/x-www-form-urlencodedのボディとして送ります。フォームの送信を参照 |
multipart | Object | 任意 | - | フォームのフィールドとファイル。multipart/form-dataのボディとして送ります。ファイルのアップロードを参照 |
cookies | Object | 任意 | - | 送るCookie。名前と値で指定します。Cookieを参照 |
trace_header | BooleanまたはString | 任意 | false | リクエストの実行、ジョブ、ステップを示すヘッダーを送ります。trueならX-Probe-Trace、文字列ならその名前のヘッダーです。リクエストのトレースを参照 |
keep_cookies | Boolean | 任意 | false | サーバーが設定したCookieをジョブで保持し、Cookieを保持する以降のステップで送ります。Cookieを参照 |
openapi | Objectまたはfalse | 任意 | - | specをパスとするOpenAPIドキュメントとリクエストおよびレスポンスを照合し、どちらかに違反があればステップを失敗させます。request: falseはレスポンスだけを照合し、strict: trueはドキュメントが宣言していないプロパティも違反にし、falseはジョブのdefaultsが求める照合を外します。OpenAPIによる検証を参照 |
リダイレクトとTLS検証を指定するパラメータはありません。リダイレクトは既定で追跡します。
headersで指定しない限り、すべてのリクエストはAccept: */*とUser-Agent: probe-http/1.0.0を送ります。ヘッダー名は大文字と小文字を区別せずに比較するため、user-agent: my-agentと書けば既定値を置き換えます。
timeout
timeoutには10sや1m30sのようなduration文字列、または秒数の数値を指定します。0を指定すると制限しません。
- name: Slow endpoint
uses: http
with:
url: "{{vars.api_url}}/report"
method: GET
timeout: 60s
test: res.code == 200制限時間を超えたリクエストはClient.Timeout exceededのエラーになり、ステップは失敗します。
ジョブのdefaultsでまとめて指定できます。
jobs:
- name: API checks
defaults:
http:
timeout: 5s
steps:
- name: Health
uses: http
with:
get: /health
test: res.code == 200ステップのtimeoutはこれとは別の、アクション実行1回ごとの外側の制限です(既定5分)。with.timeoutがHTTPリクエストそのものを、ステップのtimeoutがそれを包むアクション呼び出しを区切ります。応答を返さないまま固まったアクションを止めるのは後者です。
メソッド省略記法
get head post put patch delete connect options traceは、メソッドとパスを1つのキーで指定します。値は完全なURLか、urlからの相対パスです。ジョブのdefaultsと組み合わせると簡潔に書けます。
jobs:
- name: API checks
defaults:
http:
url: "{{vars.api_url}}"
headers:
accept: application/json
steps:
- name: List users
uses: http
with:
get: /users
test: res.code == 200
- name: Create a user
uses: http
with:
post: /users
headers:
content-type: application/json
body:
name: "{{vars.user_name}}"
test: res.code == 201フォームの送信
formは、HTMLのフォームと同じく、フィールドをapplication/x-www-form-urlencodedのボディとして送ります。フィールドの値には文字列、数値、真偽値を書けます。リストを書くと、その値ごとに同じ名前のフィールドを送ります。
- name: Log in with a form
uses: http
with:
post: /login
form:
user: "{{vars.user}}"
password: "{{vars.password}}"
scope: [read, write]
test: res.code == 302フィールドは名前順に並べてパーセントエンコードします。req.bodyには送った内容がそのまま入ります。
ファイルのアップロード
multipartは、テキストのフィールドとファイルをmultipart/form-dataのボディとして送ります。文字列、数値、真偽値で書いたフィールドはテキストのフィールドです。マップで書いたフィールドはファイルで、中身はfileのパスから読むか、contentに直接書きます。リストを書くと、その値ごとに同じ名前のフィールドを送るので、1つの名前で複数のファイルを送れます。
- name: Upload an avatar
uses: http
with:
post: /api/images
multipart:
title: avatar
image:
file: ./fixtures/logo.png
attachments:
- file: ./fixtures/terms.pdf
- content: "a,b\n1,2\n"
filename: data.csv
content_type: text/csv
test: res.code == 201ファイルには次のキーを書けます。
| キー | 説明 |
|---|---|
file | 送るファイルのパス。ほかのアクションのパスと同じく、カレントディレクトリからの相対パスです |
content | fileの代わりに送る中身 |
filename | ファイルとともに送るファイル名。既定はfileのベース名、contentの場合はフィールド名です |
content_type | ファイルのメディアタイプ。既定はfilenameの拡張子から決まるもので、決まらなければapplication/octet-streamです |
テキストのフィールドを先に、ファイルを後に、それぞれ名前順で送ります。S3へのPOSTアップロードのように、ファイルより前にあるフィールドしか読まないサーバーがあるためです。リストの値は、テキストのフィールド同士、ファイル同士の中で書いた順に送ります。テキストとファイルを混ぜたリストでは、テキストの値が先になります。ファイルはステップを実行するたびに読むので、リトライしたステップはその時点のファイルを送ります。
ボディは長さを付けて送り、表示はしません。req.bodyは空になり、代わりにreq.multipartに書いた内容が入ります。
formとmultipartはそれぞれContent-Typeヘッダーを設定し、headersやジョブのdefaultsで指定したもの(application/jsonなど)を置き換えます。どちらもbodyとは同時に指定できず、互いに同時に指定することもできません。
レスポンスオブジェクト
リクエストの後、resに応答の内容が入ります。
| フィールド | 型 | 説明 |
|---|---|---|
res.code | Integer | ステータスコード(例: 200) |
res.status | String | ステータス行(例: "200 OK") |
res.headers | Object | レスポンスヘッダー。キーはContent-Typeのような正規形 |
res.body | Any | レスポンスボディ。JSONならオブジェクトや配列に解析され、それ以外は文字列 |
res.rawbody | String | 解析前のボディ。JSONとして解析したときに入ります |
res.filepath | String | バイナリレスポンスを保存したファイルのパス |
res.cookies | Object | サーバーが設定したCookie。名前と値で入り、リダイレクトの途中で設定されたものも含みます |
res.violations | Array | リクエストとレスポンスのうちOpenAPIドキュメントが許可しないもの。すべて許可されていれば空です。openapiを指定したときだけ入ります |
res.contract | Object | OpenAPIドキュメントでレスポンスを対応付けた先。spec、GET /users/{id}のようなoperation、200、2XX、defaultのようなresponseが入ります。responseは、オペレーションがそのステータスのレスポンスを宣言していない場合は入りません。ドキュメントにリクエストのオペレーションがある場合だけ入ります |
rt.duration | String | ラウンドトリップ時間(例: "120ms") |
rt.sec | Float | ラウンドトリップ時間(秒) |
status | Integer | ステータスコードが2xxなら0、それ以外は1 |
レスポンス例
JSONレスポンスの値はres.bodyから直接読みます。
test: |
res.code == 200 &&
res.headers["Content-Type"] contains "application/json" &&
res.body.status == "ok" &&
len(res.body.items) > 0
outputs:
first_id: res.body.items[0].id
elapsed_ms: rt.sec * 1000テキストやHTMLのレスポンスでは、res.bodyは文字列そのものです。
test: |
res.code == 200 &&
res.body contains "<title>" &&
len(res.body) > 100一般的なパターン
上記のパラメータの組み合わせは、いくつかの決まった形にまとまります。トークンをステップ間で受け渡す、エラーレスポンスを検証する、まだ反映されていないリクエストをリトライする、といった形です。
認証
トークンはログインのステップの出力として取り出し、後続のステップから参照します。
- name: Log in
id: auth
uses: http
with:
url: "{{vars.api_url}}/login"
method: POST
headers:
content-type: application/json
body:
user: "{{vars.user}}"
password: "{{vars.password}}"
test: res.code == 200
outputs:
token: res.body.access_token
- name: Call a protected endpoint
uses: http
with:
url: "{{vars.api_url}}/me"
method: GET
headers:
authorization: "Bearer {{outputs.auth.token}}"
test: res.code == 200HTTP Basic認証を受け付けるサーバーには、basic_authでユーザー名とパスワードを渡します。basic_authはそこからAuthorizationヘッダーを組み立てます。ジョブのdefaultsに書けば、ジョブのすべてのリクエストに適用されます。
- name: Admin API
defaults:
http:
url: "{{vars.api_url}}"
basic_auth:
username: "{{vars.admin_user}}"
password: "{{vars.admin_password}}"
steps:
- name: List users
uses: http
with:
get: /admin/users
test: res.code == 200 && len(res.body) > 0ユーザー名にはコロンを含められません。そこでユーザー名が終わったと解釈されるためです。パスワードは空でも構いません。送れるのはどちらか一方なので、basic_authとauthorizationヘッダーは同時に指定できません。パスワードと、そこから組み立てたヘッダーは、ほかの資格情報と同じく出力では伏せられます。
Cookie
サーバーが設定したCookieは、ブラウザと同じくリダイレクト先にも送ります。そのため、セッションのCookieを設定してリダイレクトするログインでも、リダイレクト先のページまで届きます。設定されたCookieは、リダイレクトの途中で設定されたものも含めて、名前と値でres.cookiesに入ります。
keep_cookies: trueを指定すると、サーバーが設定したCookieをジョブで保持し、同じジョブの以降のステップのうち、同じくCookieを保持するステップで送ります。どのCookieをどのURLへ送るかは、ブラウザと同じくドメイン、パス、有効期限、Secure属性に従います。サーバーが削除したCookieや期限が切れたCookieは送りません。ジョブのdefaultsに書けば、ジョブのすべてのリクエストに適用されます。
- name: Logged-in pages
defaults:
http:
url: "{{vars.api_url}}"
keep_cookies: true
steps:
- name: Log in
uses: http
with:
post: /login
form:
user: "{{vars.user}}"
password: "{{vars.password}}"
test: res.code == 200 && res.cookies.session != ""
- name: My page
uses: http
with:
get: /me
test: res.code == 200Cookieはジョブごとに保持するので、同時に走るジョブ同士で互いのCookieは見えません。繰り返すジョブの各回と、embeddedアクションで実行するジョブは、Cookieを持たない状態から始まります。別のジョブへCookieを渡すには、アウトプットで渡し、渡した先でcookiesを使って送ります。
cookiesは、keep_cookiesの有無にかかわらず、名前と値でCookieを送ります。保持しているCookieに同じ名前のものがあれば、こちらで置き換えます。送り先はリクエストのホストだけで、リダイレクトで別のホストへ移ったときは送りません。cookieヘッダーを書いた場合は、それもあわせて送ります。
- name: Someone else's session
uses: http
with:
get: /me
cookies:
session: "{{outputs.other.session}}"
test: res.code == 200cookiesとres.cookiesの値は、ほかの認証情報と同じく出力では隠し、名前は表示します。
リクエストのトレース
trace_header: trueを指定すると、各リクエストに送信元を示すX-Probe-Traceヘッダーを付けます。サーバーのアクセスログで、どのリクエストかを見分けられます。
X-Probe-Trace: run=7f3a9c21e4b05d68; job=login; step=auth; repeat=0; attempt=1| フィールド | 値 |
|---|---|
run | probeの実行のID。その実行のすべてのリクエストで同じです。JSONレポートのrun_idと同じ値で、embeddedアクションで実行するジョブも同じ値を使います |
job | ジョブのID |
step | ステップのID。IDのないステップはstep_<番号>になります |
repeat | 繰り返すジョブの何回目か(0から) |
attempt | リトライするステップの何回目の試行か(1から) |
空白、セミコロン、ASCII以外の文字を含む値は、URLのクエリと同じくエスケープします。ジョブのdefaultsに書けば、ジョブのすべてのリクエストに適用されます。trueの代わりにヘッダー名を書くと、同じ値をその名前のヘッダーで送ります。サーバーが既にログに出しているヘッダーを使う場合に便利です。
- name: Traced checks
defaults:
http:
url: "{{vars.api_url}}"
trace_header: X-Correlation-Id
steps:
- name: List users
id: users
uses: http
with:
get: /users
test: res.code == 200このヘッダーはheadersに書いたヘッダーと同じく送られ、req.headersにも表示されます。headersに同じ名前のヘッダーを書いた場合はエラーになります。オプション名がtraceでないのは、traceがTRACEメソッドの省略記法だからです。
OpenAPIによる検証
openapiを指定すると、各リクエストとそのレスポンスをOpenAPIドキュメントと照合します。ステップに書いたtestとは独立に、何が正しいかをドキュメントが決めます。ジョブのdefaultsに書けば、ジョブのすべてのリクエストを照合します。
- name: Users
defaults:
http:
url: "{{vars.api_url}}"
headers:
content-type: application/json
openapi:
spec: ./openapi.yml
steps:
- name: Create a user
uses: http
with:
post: /users
body:
name: probe
test: res.body.name == "probe"
- name: Reject a user without a name
uses: http
with:
post: /users
body: {}
openapi:
request: false
test: res.code == 400
- name: Health
uses: http
with:
get: /health
openapi: false
test: res.code == 200リクエストとレスポンスは、リクエストのメソッドとパスでオペレーションに対応付けます。パスの先頭からはドキュメントのserversのパスを除きます。リクエストはステップが送ったとおりに、cookiesで送ったCookieやkeep_cookiesで保持したCookieも含めて照合し、次の場合にステップは種類contract_requestで失敗します。
- パス、クエリ、ヘッダー、Cookieのパラメータが、必須なのにない、またはスキーマに合わない
- ボディが、必須なのにない、
Content-Typeがオペレーションの宣言する型ではない、またはスキーマに合わない - オペレーションの
securityが求める認証情報がない
レスポンスは、リダイレクトされた場合は最後のリクエストで照合し、次の場合にステップは種類contract_responseで失敗します。
- メソッドとパスに対応するオペレーションがドキュメントにない
- オペレーションがそのステータスコードも
defaultのレスポンスも宣言していない - レスポンスの
Content-Typeが、そのレスポンスの宣言する型ではない - レスポンスが宣言するヘッダー、またはボディがスキーマに合わない
- JSONのボディが空、またはスキーマがnullを許可しないのに
null
ボディはJSON、テキスト、XML、YAML、CSV、フォームとして照合します。画像やPDFなどほかの型のボディは、Content-Typeだけを照合します。
両方に違反がある場合、種類はcontract_requestです。ワークフローがドキュメントの許可しないものを送っており、それがレスポンスの違反の原因かもしれないからです。testが成り立っていても失敗し、testは評価しません。testのないステップは、リクエストとレスポンスがドキュメントに合っていれば成功します。ドキュメントが検証したからです。retryを指定すれば、testが成り立つまで再試行するのと同じく、リクエストとレスポンスがドキュメントに合うまで再試行します。違反はそれぞれres.violations、端末、レポートに{in, field, reason, message}の形で入ります。inはrequestかresponseで、fieldは$.idのようなボディのフィールドです。fieldは該当するときだけ入ります。
拒否されることを確かめるために、ドキュメントの許可しないものをわざと送るステップにはrequest: falseを書きます。レスポンスは引き続き照合するため、ドキュメントが宣言していない拒否の仕方をすればステップは失敗します。securityが求める認証情報なしで送るリクエストもこれにあたり、401を確かめる場合などに使います。
strict
スキーマは通常、宣言していないプロパティも許可します。そのため、たとえばpassword_hashまで返すレスポンスもスキーマに合っていることになります。strict: trueを指定すると、次の場合にもステップは失敗します。
- JSONのボディに、スキーマが宣言していないプロパティがある。対象は、プロパティか
patternPropertiesを書き、additionalPropertiesを書いていないスキーマです - オペレーションが宣言していないクエリパラメータがある
- リクエストに
readOnlyのプロパティ、レスポンスにwriteOnlyのプロパティがある
openapi:
spec: ./openapi.yml
strict: trueadditionalPropertiesを書いたスキーマは、ほかに何を許可するかを自ら示しています。trueやスキーマ(labelsのようなマップ)はほかのプロパティを許可し、falseはstrictがなくても違反になります。type: objectとだけ書いたような、プロパティを何も宣言していないスキーマは何でも許可します。allOf、oneOf、anyOfのスキーマが宣言するプロパティは、宣言されたものとして扱います。ヘッダーとCookieはstrictに照合しません。ServerやロードバランサーのCookieなど、プロキシ、サーバー、クライアントが付けるものをドキュメントが宣言することはまれだからです。違反は見つかった場所によってcontract_requestかcontract_responseになり、fieldには$.owner.emailのようにプロパティが入ります。
specはProbeを実行したディレクトリからのパスです。ドキュメントはリクエストを送る前に読むため、読めない、または解析できないドキュメントを指定したステップは、何も送らずにアクションのエラーとして失敗します。ステップにopenapi: falseを書くと、ドキュメントが扱わないエンドポイントなどで照合を外せます。
各ステップのレスポンスを対応付けたオペレーションとレスポンスは、res.contractとJSONレポートに入ります。probe coverageは、そのレポートから、ドキュメントのうちどのステップも検証していないものを示します。
ガードの下での動作
--read-onlyを指定して実行すると、GET、HEAD、OPTIONSだけを送り、それ以外のメソッドは送る前に拒否します。--allow-hostを指定して実行すると、許可されていないホストへのURLやリダイレクトを拒否します。ポートのないURLは、スキームの既定のポートとして扱います。拒否されたステップは種類refusedで失敗します。--read-onlyを参照してください。
エラーレスポンスの検証
ここではエラーが期待される結果です。そのためステータスとエラーのボディを検証します。
- name: Unknown id returns 404
uses: http
with:
url: "{{vars.api_url}}/users/does-not-exist"
method: GET
test: res.code == 404 && res.body.error != null不安定なエンドポイントのリトライ
retryは、テストが通るか試行回数を使い切るまでステップを繰り返します。
- name: Eventually consistent read
uses: http
retry:
max_attempts: 5
interval: 2s
with:
url: "{{vars.api_url}}/orders/{{outputs.create.order_id}}"
method: GET
test: res.code == 200