Closed34

tagpr で 'The search is longer than 256 characters' とエラー出たが GitHub の Search API の Validation が謎だった

snakasnaka

tagpr を利用しているリポジトリの workflow で以下のようなエラーが発生した
(Org名/リポジトリ名は伏せている)

GET https://api.github.com/search/issues?q=repo%3Afoo%2Fbar+is%3Apr+is%3Aclosed+a3dbce5+efe8b21+33b2841+57329cf+ae236ff+c154ae2+ce4d1f9+571a330+6296351+50d269e+597c876+5b591fb+acc2876+ecb195d+b6032a1+3ba8c8d+fdce965+c1e99b3+dbccbab+22911dc+13da159+16e3d88+08eb384+bccdaa6+b1172ba+dbb2169: 422 Validation Failed [{Resource:Search Field:q Code:invalid Message:The search is longer than 256 characters.}]

クエリ部分を読みやすくデコードすると以下のようになる

repo:foo/bar is:pr is:closed a3dbce5 efe8b21 33b2841 57329cf ae236ff c154ae2 ce4d1f9 571a330 6296351 50d269e 597c876 5b591fb acc2876 ecb195d b6032a1 3ba8c8d fdce965 c1e99b3 dbccbab 22911dc 13da159 16e3d88 08eb384 bccdaa6 b1172ba dbb2169
snakasnaka
snakasnaka

再代入があったりしてロジックが読みにくいが以下のことを行っているらしい

  • chunkQueries 配列には query 文字列が入る
  • query 文字列は以下の構成
    • repos:foo/bar is:pr is:closed // qualifier
    • (sha) (sha) (sha) ... // commit sha の羅列 (256 文字を超えない範囲で詰める)
snakasnaka

GitHub API の制限を調べる

https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#limitations-on-query-length

query の長さ制限について

Limitations on query length

You cannot use queries that:

  • are longer than 256 characters (not including operators or qualifiers).
  • have more than five AND, OR, or NOT operators.

These search queries will return a "Validation failed" error message.

query の構造

A query can contain any combination of search qualifiers supported on GitHub. The format of the search query is:

SEARCH_KEYWORD_1 SEARCH_KEYWORD_N QUALIFIER_1 QUALIFIER_N
snakasnaka

エラーになった query で以下の部分は qualifier

repo:foo/bar is:pr is:closed

以下の部分が search keyword

a3dbce5 efe8b21 33b2841 ... (snip)
snakasnaka

再現してみる

#!/bin/bash

# GitHub API endpoint
url="https://api.github.com/search/issues"

# search query
qualifiers="repo:foo/bar is:pr is:closed"
# keywords="a3dbce5 efe8b21 33b2841 57329cf ae236ff c154ae2 ce4d1f9 571a330 6296351 50d269e 597c876 5b591fb acc2876 ecb195d b6032a1 3ba8c8d fdce965 c1e99b3 dbccbab 22911dc 13da159 16e3d88 08eb384 bccdaa6 b1172ba dbb2169" # NG
keywords="e9c8744 9730145 cbf227f e9a9a1e 7749956 593178a 7df9cae 01ae53d a607855 bd7d0c5 4610ac3 baf17b3 2e61f3c ed3a4be 75a6513 2559d2a 52dea6f 2887d77 c31f660 95f0247 3b35393 63eafc4 3144034 b7a8414 ad35d6f 298e57a" # OK

echo "keywords (${#keywords}): $keywords"
echo "---------------------------------"

query="$qualifiers $keywords"

# URL encode
encoded_query=$(echo "$query" | jq -sRr @uri)

# run curl command
curl -i -H "Accept: application/vnd.github.v3+json" \
     -H "Authorization: token $GH_TOKEN" \
     "$url?q=$encoded_query"
snakasnaka

keywords の部分が同じ長さに見えるけど、エラーになるケースとならないケースある

エラーにならない

keywords (207): e9c8744 9730145 cbf227f e9a9a1e 7749956 593178a 7df9cae 01ae53d a607855 bd7d0c5 4610ac3 baf17b3 2e61f3c ed3a4be 75a6513 2559d2a 52dea6f 2887d77 c31f660 95f0247 3b35393 63eafc4 3144034 b7a8414 ad35d6f 298e57a

結果

{
  "total_count": 0,
  "incomplete_results": false,
  "items": [

  ]
}
snakasnaka

エラーになる

keywords (207): a3dbce5 efe8b21 33b2841 57329cf ae236ff c154ae2 ce4d1f9 571a330 6296351 50d269e 597c876 5b591fb acc2876 ecb195d b6032a1 3ba8c8d fdce965 c1e99b3 dbccbab 22911dc 13da159 16e3d88 08eb384 bccdaa6 b1172ba dbb2169

結果

{
  "message": "Validation Failed",
  "errors": [
    {
      "message": "The search is longer than 256 characters.",
      "resource": "Search",
      "field": "q",
      "code": "invalid"
    }
  ],
  "documentation_url": "https://docs.github.com/v3/search/",
  "status": "422"
}
snakasnaka

これは検証方法がミスっている

前述のスクリプトで発行しているクエリのスペースの区切り方が本来のものと違っていること気付いた...

snakasnaka

スクリプトを修正

#! /bin/bash
# To run this script, the environment variable GH_TOKEN must be set for tokens with access to repositories such as PAT.

qualifiers="repo%3Asnaka%2Ftagpr-err-reproduce+is%3Apr+is%3Aclosed"
keywords="a3dbce5+efe8b21+33b2841+57329cf+ae236ff+c154ae2+ce4d1f9+571a330+6296351+50d269e+597c876+5b591fb+acc2876+ecb195d+b6032a1+3ba8c8d+fdce965+c1e99b3+dbccbab+22911dc+13da159+16e3d88+08eb384+bccdaa6+b1172ba+dbb2169"    # 1. NG
query="${qualifiers}+${keywords}"

# < Replacing the following keyword with the line above confirms the instability of the results. >
#
# keywords="a3dbce5+efe8b21+33b2841+57329cf+ae236ff+c154ae2+ce4d1f9+571a330+6296351+50d269e+597c876+5b591fb+acc2876+ecb195d+b6032a1+3ba8c8d+fdce965+c1e99b3+dbccbab+22911dc+13da159+16e3d88+08eb384+bccdaa6+b1172ba+dbb2169"    # 1. NG
# keywords="a3dbce5+efe8b21+33b2841+57329cf+ae236ff+c154ae2+ce4d1f9+571a330+6296351+50d269e+597c876+5b591fb+acc2876+ecb195d+b6032a1+3ba8c8d+fdce965+c1e99b3+dbccbab+22911dc+13da159+16e3d88+08eb384+bccdaa6+b1172ba+dbb216"     # 2. OK
# keywords="e9c8744+9730145+cbf227f+e9a9a1e+7749956+593178a+7df9cae+01ae53d+a607855+bd7d0c5+4610ac3+baf17b3+2e61f3c+ed3a4be+75a6513+2559d2a+52dea6f+2887d77+c31f660+95f0247+3b35393+63eafc4+3144034+b7a8414+ad35d6f+298e57a"    # 3. OK
# keywords="e9c8744+9730145+cbf227f+e9a9a1e+7749956+593178a+7df9cae+01ae53d+a607855+bd7d0c5+4610ac3+baf17b3+2e61f3c+ed3a4be+75a6513+2559d2a+52dea6f+2887d77+c31f660+95f0247+3b35393+63eafc4+3144034+b7a8414+ad35d6f+298e57abcd" # 4. OK
# keywords="0000000+000000a+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa"    # 5. NG
# keywords="0000000+000000a+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaaa+aaaaaa"     # 6. OK

# The API specification states 'not including operators or qualifiers', so the number of
# characters in the non-qualifiers part (keywords) is counted.

echo "keywords char length: ${#keywords}"
echo "query char length:    ${#query}"
echo "---------------------------------"

curl -H "Accept: application/vnd.github.v3+json" \
  -H "Authorization: token $GH_TOKEN" \
  "https://api.github.com/search/issues?q=${query}"
snakasnaka

とりあえず回避してみる

問題の発生状況

  • 前回のタグから最新まで commit を取得している
  • 初回リリースに含まれる commit 数が多い
  • 検索する commit 件数が検索APIの query の最大長に収まっている(はず)にもかかわらず、 API 側から「256 文字を超えている」という判定結果が返ってくる

対応

  • リリースに含まれる commit 件数を削減する
snakasnaka

リリースに含まれる commit 件数を削減する

tagpr は前回リリース(tag)以降積まれた default ブランチの commit を探しに行く。

つまり、前回リリース(tag)が最新 commit に近いほど探索する数が減って、前述の問題は回避できるはず。

初回リリースの v0.0.1 に対する「前回」は v0.0.0 のはず(?)なので、明示的に v0.0.0 のタグを打ってみたら問題が解消した。

snakasnaka

初回はこれで良かったけど、リリース運用した後同じことが発生すると詰んでしまいそう...

snakasnaka

quailfier として与えるリポジトリ名が文字数制限に影響するかどうか確認する

仕様によると qualifier は文字数の制限に含まれないと理解しているが、本当にそうなのか?

https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#limitations-on-query-length

snakasnaka

clone した方のリポジトリの workflow でエラーが出てた

POST https://api.github.com/repos/snaka/tagpr-err-reproduce-with-veeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long-repository-namery-long/releases/generate-notes: 403 Resource not accessible by integration []
snakasnaka

API のバリデーション結果を見る限り、qualifier に含まれているリポジトリ名の長さには関係が無さそう。
つまり、検索ワードのバリデーションには qualifier は含まれていない ( そう書いていた )

snakasnaka

エラーを再現させてみる

tagpr のロジックでは qualifier も含めて文字数を計算している。

その計算の前提が間違っているとしたら、リポジトリ名が極端に短い場合は確実にエラーを再現できるはず。

snakasnaka

再現できた

GET https://api.github.com/search/issues?q=repo%3Asnaka%2Fa+is%3Apr+is%3Aclosed+0aa9d3e+10e7dfc+b8c0d32+a60b1f9+67c2d72+8750b6e+9a6fa3d+ed6313d+e9c8744+9730145+cbf227f+e9a9a1e+7749956+593178a+7df9cae+01ae53d+a607855+bd7d0c5+4610ac3+baf17b3+2e61f3c+ed3a4be+75a6513+2559d2a+52dea6f+2887d77+c31f660+95f0247: 422 Validation Failed [{Resource:Search Field:q Code:invalid Message:The search is longer than 256 characters.}]

https://github.com/snaka/a/actions/runs/10215032050/job/28263612046#step:3:77

snakasnaka

Go でURIエスケープの方法を確認する

https://go.dev/play/p/0Xk56v95-Ol?v=goprev

snakasnaka
// You can edit this code!
// Click here and start typing.
package main

import (
	"fmt"
	"net/url"
)

func main() {
	qualifier := "repo:snaka/a is:pr is:closed"
	keywords := "10e7dfc b8c0d32 a60b1f9 67c2d72 8750b6e 9a6fa3d"

	query := qualifier + " " + keywords
	fmt.Println("query:", query)

	fmt.Println("escaped(query):", url.QueryEscape(query))
	fmt.Println("escaped(path) :", url.PathEscape(query))
}

実行結果

query: repo:snaka/a is:pr is:closed 10e7dfc b8c0d32 a60b1f9 67c2d72 8750b6e 9a6fa3d
escaped(query): repo%3Asnaka%2Fa+is%3Apr+is%3Aclosed+10e7dfc+b8c0d32+a60b1f9+67c2d72+8750b6e+9a6fa3d
escaped(path) : repo:snaka%2Fa%20is:pr%20is:closed%2010e7dfc%20b8c0d32%20a60b1f9%2067c2d72%208750b6e%209a6fa3d
snakasnaka

上の結果から、
スペースを%20 に変換するのには url.PathEscape のほうが使えそうだが、 query 文字列のエスケープに PathEscape を使う気持ち悪さもある

得られる結果がたまたま同じという理由で、違う用途のものを使うのに抵抗ある。

おもそも、APIにおける文字数カウント方法についてのロジックが公開されていない状態で、 PathEscape がそのAPI側の仕様にマッチしているという保証も無く、逆に困る場面も無いとは言えない。

snakasnaka

普通に %20 で join するだけでも良さそう

真偽不明な仕様を持ち込むより、上限に余裕をもたせる方針に着地した。

このスクラップは5ヶ月前にクローズされました