リクエストヘッダーとリクエストボディの読み取りと書き込み

Flex Gateway ポリシー開発キット (PDK) を使用すると、リクエスト/レスポンスヘッダーおよびボディの読み取りと書き込みを行うことができます。

イベントフロー

要求および応答を絞り込むときに、Proxy Wasm はヘッダーとボディの処理を必ず特定の順序で発生する 2 つのメインイベントに分割します。ポリシーでボディを処理する前に、すべてのポリシーでヘッダーイベントが完全に伝播されている必要があります。たとえば、2 つのポリシーが適用される API のイベントフローは次のようになります。

  1. ポリシー 1 はリクエストヘッダーイベントを処理します。

  2. ポリシー 2 はリクエストヘッダーイベントを処理します。

  3. バックエンドサービスはリクエストヘッダーを受信します。

  4. ポリシー 1 はリクエストボディイベントを処理します。

  5. ポリシー 2 はリクエストボディイベントを処理します。

  6. バックエンドはリクエストボディを受信します。

  7. バックエンドは応答を送信します。

  8. ポリシー 2 はレスポンスヘッダーイベントを処理します。

  9. ポリシー 1 はレスポンスヘッダーイベントを処理します。

  10. クライアントはレスポンスヘッダーを受信します。

  11. ポリシー 2 はレスポンスボディイベントを処理します。

  12. ポリシー 1 はレスポンスボディイベントを処理します。

  13. クライアントはレスポンスボディを受信します。

この順序では、次の制限が生じます。

  • ポリシーはボディを読み取った後にヘッダーを変更できない。

  • すべてのポリシーはボディを読み取る前にヘッダーイベントを完全に処理する必要がある。ボディのデータをあるポリシーに保存し、別のポリシーのボディデータを使用してヘッダーを変更することはできません。

  • すべてのポリシーはヘッダーイベントを処理するときにバックエンドサービスに到達する。拒否された要求からのデータがバックエンドサービスに到達しないようにするには、リクエストヘッダーのみに基づいてヘッダーを拒否する必要があります。ボディに基づいて要求を拒否すると、大部分のサーバーおよびクライアントは中断された要求を検出して、リクエストヘッダーを破棄できます。

ポリシーソースコードでボディを読み取ると、ヘッダーとボディのイベントが分割されます。たとえば、ボディを読み取るために宣言 ​let body_state = headers_state.into_body_state().await;​ が使用されている場合、宣言前のすべてがヘッダーイベントで発生し、宣言後のすべてがボディイベントで発生します。

リクエストヘッダーの読み取りと書き込み

リクエストヘッダーとレスポンスヘッダーの両方にアクセスするには、​into_headers_state()​ メソッドを呼び出して、​RequestState​ または ​ResponseState​ をヘッダー状態に変換して、処理が完了するのを待ちます。メソッドを呼び出したら、​HeadersHandler​ trait の関数を呼び出してヘッダーにアクセスして操作します。

pub trait HeadersHandler {
    fn headers(&self) -> Vec<(String, String)>;
    fn header(&self, name: &str) -> Option<String>;
    fn add_header(&self, name: &str, value: &str);
    fn set_header(&self, name: &str, value: &str);
    fn set_headers(&self, headers: Vec<(&str, &str)>);
    fn remove_header(&self, name: &str);
}

以下のコードを実装することで、ラップされた関数の ​on_request​ または ​on_response​ でヘッダーにアクセスすることができます。

async fn request_filter(request_state: RequestState, _config: &Config) {
    let headers_state = request_state.into_headers_state().await;
    let headers_handler = headers_state.handler();
    let old_value = headers_handler.header("request-header").unwrap_or_default();

    let new_value = "--replaced--";
    logger::info!("Old request header value: {old_value}, New value: {new_value}");
    headers_handler.set_header("request-header", new_value);
}

async fn response_filter(response_state: ResponseState, _config: &Config) {
    let headers_state = response_state.into_headers_state().await;
    let headers_handler = headers_state.handler();
    let old_headers = headers_handler.header("request-header").unwrap_or_default();

    let new_value = vec![("response-header1", "--replaced--"), ("response-header2", "--replaced--")];
    logger::info!("Old request header value: {old_headers:?}, New value: {new_value:?}");
    headers_handler.set_headers(new_value);
}

Envoy はメソッド、スキーム、パス、承認、状況コードをヘッダーとして扱います。PDK は要求の Envoy ヘッダーである ​:method​、​:scheme​、​:path​、​:authority​、​:status​ にアクセスして変更するために以下のメソッドを提供しています。

async fn request_filter(request_state: RequestState, _config: &Config) {
    let headers_state = request_state.into_headers_state().await;
    let method = headers_state.method();
    let scheme = headers_state.scheme();
    let authority = headers_state.authority();
    let path = headers_state.path();
    ...
}

async fn response_filter(response_state: ResponseState, _config: &Config) {
    let headers_state = response_state.into_headers_state().await;
    let status = headers_state.status_code();
}

リクエストボディの読み取りと書き込み

ボディの読み取りと書き込みのペイロードサイズは 1MB 未満に制限されています。

リクエストボディとレスポンスボディの両方にアクセスするには、​into_body_state()​ メソッドを呼び出して、​RequestState​ または ​ResponseState​ をボディ状態に変換して、処理が完了するのを待ちます。

let body_state = request_state.into_body_state().await;
前の ​.await​ は、キャンセルポイントになります。要求中に​フローがキャンセル​されると、​.await​ が再開されない可能性があります。

元の状態がすでにヘッダー状態に変換されている場合は、同じ関数を呼び出してボディ状態に変換します。例:

let headers_state = request_state.into_headers_state().await;
let body_state = headers_state.into_body_state().await;

into_body_state()​ を呼び出したら、​BodyHandler​ trait の関数を呼び出してヘッダーにアクセスして操作します。

pub trait BodyHandler {
    fn body(&self) -> Vec<u8>;
    fn set_body(&self, body: &[u8]) -> Result<(), BodyError>;
}

Envoy はヘッダーとボディのデータを共有するために同じバッファを使用するため、ポリシーはヘッダーとボディに同時にアクセスすることはできません。もしポリシーが両方を読み取る必要がある場合は、次の手順に従ってください。

  1. ヘッダーを読み取り、必要な値を変数に保存します。

  2. ボディを読み取ります。

応答と要求の両方で、ヘッダーとボディを読み取ることができます。ただし、ボディを読み取った後でヘッダーを変更することはできません。ボディを読み取る前にすべてのヘッダーの修正を完了してください。例:

async fn request_filter(request_state: RequestState) {
    let headers_state = request_state.into_headers_state().await;
    let headers_handler = headers_state.handler();

    let agent = headers_handler.header("User-Agent").unwrap_or_else(|| "Undefined".to_string());

    // Removing old content length header before manipulating body
    headers_handler.remove_header("content-length");

    let body_state = headers_state.into_body_state().await;
    let body_handler = body_state.handler();
    let body = body_handler.body();

    logger::info!("User: {agent} sent: {}", String::from_utf8_lossy(body.as_slice()));

    let new_body = "new body".as_bytes();
    match body_handler.set_body(&new_body) {
        Ok(_) => logger::info!("Body updated"),
        Err(e) => logger::info!("Unable to set body. Reason: {e:?}),
    }
}
このコードでは ​content-length​ ヘッダーを削除します。これはボディを修正するために必要です。

BodyHandler::set_body()​ メソッドは、​Result<(), BodyError>​ オブジェクトを返します。ボディの更新は、以下が原因で失敗する可能性があります。

  • BodyError::BodyNotSent​: 現在の HTTP フローにボディ (​GET​ 要求など) がありません。

  • BodyError::ExceededBodySize​: 新しいボディが Envoy でサポートされる最大ボディバッファサイズを超えています。