curl 프로젝트가 배포하는 curl 공식 로고를 비율 그대로 줄여 두고 아래에 글 제목을 적은 이미지

curl: (3) bad range, 워드프레스 플러그인 API의 대괄호 때문

워드프레스 플러그인 정보의 공식 입구는 plugins_api()가 부르는 https://api.wordpress.org/plugins/info/1.2/입니다. action은 plugin_information이고, 슬러그는 request[slug]로 넘깁니다. AI 플러그인을 배포할 때 이 디렉터리 API 조회가 Debian 13에서 실제로 되는지, 호스트의 워드프레스·PHP 조건과는 어떻게 맞춰 볼지를 세 스크립트로 실행해 기록했습니다. 운영 서버에 플러그인을 더할 때는 설치 화면을 열기 전에 배포 버전과 요구 환경을 표로 만들어 두면 변경 검토가 훨씬 수월합니다.

그 대괄호를 쿼리 문자열에 그대로 넣고 curl을 돌리면 HTTP 응답이 오기도 전에 curl: (3) bad range로 멈춥니다. Debian 13에서 이 오류를 재현한 뒤, –globoff와 –data-urlencode로 ai-engine, jetpack, getgenie, seo-by-rank-math를 조회하고, 1.0 JSON과 readme·메인 PHP 헤더가 서로 다른 값을 내는 지점까지 따라갔습니다.

request[slug]의 대괄호에서 curl이 (3) bad range로 멈춤

재현 환경은 Debian GNU/Linux 13이고, 실행일과 도구 버전을 아래 표에 모았습니다. curl 버전은 8.14.1이며, 표의 그 칸에는 실행 출력의 첫 줄을 그대로 옮겼습니다. 서버에서 같은 오류를 만났다면 이 환경 정보와 쓰고 있는 도구 버전부터 나란히 놓아 보면 됩니다.

항목값
실행일2026-10-14
OSDebian GNU/Linux 13 (trixie)
커널Linux 6.12.94+ x86_64
nproc8
RAMRAM_TOTAL_GB 15
PythonPython 3.13.5
curlcurl 8.14.1 (x86_64-pc-linux-gnu) libcurl/8.14.1 OpenSSL/3.5.7 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 nghttp3/1.8.0 librtmp/2.3 OpenLDAP/2.6.10

환경 정보를 찍은 다음 ai-engine을 조회하도록 명령을 짰습니다. 마지막 URL은 1.2 주소에 request[slug]를 그대로 붙여 큰따옴표로 감싼 형태입니다. 이 요청에는 –globoff도 –data-urlencode도 없어서, 대괄호가 curl에 그대로 넘어갈 때 어떻게 되는지가 드러납니다.

date +%Y-%m-%d
. /etc/os-release && printf '%s\n' "$PRETTY_NAME"
uname -srm
nproc
free -g | awk 'NR==2{print "RAM_TOTAL_GB", $2}'
python3 --version
curl --version | head -n 1
curl --silent --show-error --max-time 15 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine"

실행 결과입니다. Debian GNU/Linux 13에서 curl 8.14.1이 쓰였고 RAM_TOTAL_GB는 15로 찍혔습니다. 마지막 조회는 curl_exit:3으로 끝났으니, 환경 정보를 모은 것은 성공이고 API 조회는 실패입니다.

$ date +%Y-%m-%d
2026-10-14
$ . /etc/os-release && printf %s\n "$PRETTY_NAME"
Debian GNU/Linux 13 (trixie)
$ uname -srm
Linux 6.12.94+ x86_64
$ nproc
8
$ free -g | awk 'NR==2{print "RAM_TOTAL_GB", $2}'
RAM_TOTAL_GB 15
$ python3 --version
Python 3.13.5
$ curl --version | head -n 1
curl 8.14.1 (x86_64-pc-linux-gnu) libcurl/8.14.1 OpenSSL/3.5.7 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 nghttp3/1.8.0 librtmp/2.3 OpenLDAP/2.6.10
$ command -v nvidia-smi || echo nvidia-smi: command not found
nvidia-smi: command not found
$ curl --silent --show-error --max-time 15 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine"
curl: (3) bad range in URL position 79:
https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine
                                                                              ^
curl_exit:3

출력의 캐럿은 URL에서 request[slug]가 들어 있는 자리를 가리킵니다. curl: (3) bad range가 났고 종료 값도 3입니다. 이어지는 캡처가 그 터미널 화면이고, 뒤의 고친 요청과 비교할 기준이 됩니다.

워드프레스 플러그인 API 실행 화면: curl이 URL의 bad range 오류를 내고 종료 값 3으로 끝납니다
2026-10-14 14:08:55 KST, Debian 13 — curl이 URL의 bad range 오류를 내고 종료 값 3으로 끝납니다.

환경 정보 출력에 이어 curl이 URL을 처리하다 멈추는 장면입니다. 오류 원문은 curl: (3) bad range in URL position 79이고 종료 줄은 curl_exit:3입니다. 원격 API가 살아 있는지를 따지기 전에, 요청을 만드는 쪽의 URL 처리부터 고쳐야 하는 단계입니다.

원인은 URL의 대괄호와 curl의 글로브 처리입니다. 주소를 큰따옴표로 감쌌는데도 이번 요청은 curl_exit:3으로 끝났으니, 셸 인용으로 풀렸다고 믿으면 같은 문제를 또 만납니다. 그래서 –globoff를 붙여 주소를 그대로 두는 방법과 –data-urlencode로 인자를 넘기는 방법을 같은 조회 결과로 비교했습니다.

--globoff로 글로브를 끄거나 --data-urlencode로 인자를 넘기기

먼저 주소 모양은 그대로 두고 –globoff로 글로브만 끕니다. sections는 0, active_installs와 downloaded는 1로 요청하고 필요한 키만 찍습니다. 긴 sections HTML을 받지 않으려고 sections=0을 넣었습니다.

parse_keys='import json,sys
d=json.load(sys.stdin)
keys=["slug","name","version","requires","requires_php","tested","last_updated","rating","num_ratings","active_installs","downloaded"]
for k in keys:
    print(k+"="+json.dumps(d.get(k), ensure_ascii=False))
print("has_active_installs="+("yes" if "active_installs" in d else "no"))
'
curl --globoff --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 \
  "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine&request[fields][sections]=0&request[fields][active_installs]=1&request[fields][downloaded]=1" \
  | python3 -c "$parse_keys"

이 호출의 globoff_exit는 0이고 has_active_installs는 yes입니다. ai-engine의 version은 3.8.4, requires는 6.0, requires_php는 8.1로 돌아왔습니다. 주소를 그대로 써야 하는 점검 명령이라면 –globoff를 같이 보관해 둬야 bad range를 다시 만나지 않습니다. name에는 HTML 엔티티가 남아 있으니, 화면에 보일 이름과 호스트 조건을 판정할 필드는 따로 다룹니다.

이 방식은 주소 한 줄을 그대로 둔 채 플래그 하나로 글로브를 끄니, 이미 만들어 둔 plugin_information URL에 바로 붙일 수 있습니다. sections, active_installs, downloaded도 같은 쿼리에서 지정할 수 있고, 이번 ai-engine 호출은 종료 코드 0으로 필드를 돌려줬습니다. 다만 주소에 대괄호가 남아 있어서 –globoff를 빼먹는 순간 재현 스크립트와 같은 bad range가 다시 나고, 필드가 늘어나면 한 줄 쿼리에서 괄호 짝을 눈으로 맞추기가 어려워집니다.

다른 방법은 베이스 URL에 대괄호를 아예 넣지 않는 것입니다. –get과 –data-urlencode로 action, request[slug], sections, active_installs, downloaded를 하나씩 넘기고, 이 형태에는 –globoff가 없습니다. 같은 함수로 네 슬러그를 돌립니다.

query_encoded() {
  slug="$1"
  echo "$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get https://api.wordpress.org/plugins/info/1.2/ --data-urlencode action=plugin_information --data-urlencode request[slug]=${slug} --data-urlencode request[fields][sections]=0 --data-urlencode request[fields][active_installs]=1 --data-urlencode request[fields][downloaded]=1 | python3 -c \"\$parse_keys\""
  curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get \
    "https://api.wordpress.org/plugins/info/1.2/" \
    --data-urlencode "action=plugin_information" \
    --data-urlencode "request[slug]=${slug}" \
    --data-urlencode "request[fields][sections]=0" \
    --data-urlencode "request[fields][active_installs]=1" \
    --data-urlencode "request[fields][downloaded]=1" \
    | python3 -c "$parse_keys"
  echo "encoded_exit:${slug}:$?"
}

query_encoded ai-engine
query_encoded jetpack
query_encoded getgenie
query_encoded seo-by-rank-math

함수는 슬러그만 바꾸고 요청 필드와 파서는 그대로 두도록 짰습니다. 출력을 보면 ai-engine은 –globoff 방식과 인코딩 방식 모두에서 version 3.8.4와 requires_php 8.1을 돌려줍니다. 나머지 슬러그도 같은 모양으로 조회되니, 서로 다른 필드 집합을 비교하다 헷갈릴 일이 없습니다.

$ curl --globoff --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine&request[fields][sections]=0&request[fields][active_installs]=1&request[fields][downloaded]=1" | python3 -c "$parse_keys"
slug="ai-engine"
name="AI Engine – The Chatbot, AI Framework & MCP for WordPress"
version="3.8.4"
requires="6.0"
requires_php="8.1"
tested="7.1.3"
last_updated="2026-10-08 6:29am GMT"
rating=98
num_ratings=870
active_installs=90000
downloaded=7886149
has_active_installs=yes
globoff_exit:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get https://api.wordpress.org/plugins/info/1.2/ --data-urlencode action=plugin_information --data-urlencode request[slug]=ai-engine --data-urlencode request[fields][sections]=0 --data-urlencode request[fields][active_installs]=1 --data-urlencode request[fields][downloaded]=1 | python3 -c "$parse_keys"
slug="ai-engine"
name="AI Engine – The Chatbot, AI Framework & MCP for WordPress"
version="3.8.4"
requires="6.0"
requires_php="8.1"
tested="7.1.3"
last_updated="2026-10-08 6:29am GMT"
rating=98
num_ratings=870
active_installs=90000
downloaded=7886149
has_active_installs=yes
encoded_exit:ai-engine:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get https://api.wordpress.org/plugins/info/1.2/ --data-urlencode action=plugin_information --data-urlencode request[slug]=jetpack --data-urlencode request[fields][sections]=0 --data-urlencode request[fields][active_installs]=1 --data-urlencode request[fields][downloaded]=1 | python3 -c "$parse_keys"
slug="jetpack"
name="Jetpack – WP Security, Backup, Speed, & Growth"
version="16.3.1"
requires="7.0"
requires_php="7.4"
tested="7.1.3"
last_updated="2026-10-12 6:15pm GMT"
rating=74
num_ratings=2407
active_installs=3000000
downloaded=514130577
has_active_installs=yes
encoded_exit:jetpack:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get https://api.wordpress.org/plugins/info/1.2/ --data-urlencode action=plugin_information --data-urlencode request[slug]=getgenie --data-urlencode request[fields][sections]=0 --data-urlencode request[fields][active_installs]=1 --data-urlencode request[fields][downloaded]=1 | python3 -c "$parse_keys"
slug="getgenie"
name="GetGenie – AI SEO Assistant & Content Writer with Keyword Research, AEO & GEO"
version="4.5.6"
requires=false
requires_php="7.4"
tested="7.1.3"
last_updated="2026-09-07 9:46am GMT"
rating=96
num_ratings=119
active_installs=80000
downloaded=1087850
has_active_installs=yes
encoded_exit:getgenie:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --get https://api.wordpress.org/plugins/info/1.2/ --data-urlencode action=plugin_information --data-urlencode request[slug]=seo-by-rank-math --data-urlencode request[fields][sections]=0 --data-urlencode request[fields][active_installs]=1 --data-urlencode request[fields][downloaded]=1 | python3 -c "$parse_keys"
slug="seo-by-rank-math"
name="Rank Math SEO – AI SEO Tools to Dominate SEO Rankings"
version="1.0.280"
requires="6.7"
requires_php="7.4"
tested="7.1.3"
last_updated="2026-10-06 12:43pm GMT"
rating=96
num_ratings=7513
active_installs=4000000
downloaded=200410164
has_active_installs=yes
encoded_exit:seo-by-rank-math:0
$ for u in ...; do curl -sI --max-time 15 -A ainewsnote-guide/2026-10-14 -o /dev/null -w "%{http_code} %{url_effective}\n" "$u"; done
200 https://api.wordpress.org/plugins/info/1.2/
200 https://developer.wordpress.org/reference/functions/plugins_api/
200 https://wordpress.org/plugins/ai-engine/
200 https://wordpress.org/plugins/jetpack/
200 https://wordpress.org/plugins/getgenie/
200 https://wordpress.org/plugins/seo-by-rank-math/

긴 출력에는 플러그인별 응답 뒤에 encoded_exit가 붙어 있어 반복 조회의 결과를 하나씩 따라갈 수 있습니다. 네 호출 모두 종료 값 0이고 active_installs와 downloaded가 함께 왔습니다. 이어지는 캡처가 이 인코딩 조회 화면이고, 본문에 옮긴 숫자는 출력의 각 슬러그 행과 맞춰 보면 됩니다.

워드프레스 플러그인 API 실행 화면: 인코딩 조회에서 Rank Math의 active_installs=4000000을 얻습니다
2026-10-14 14:09:24 KST, Debian 13 — 인코딩 조회에서 Rank Math의 active_installs=4000000을 얻습니다.

캡처에 해당하는 출력에서 네 슬러그의 encoded_exit는 모두 0, has_active_installs는 yes입니다. Rank Math의 active_installs는 4000000이고, 이는 seo-by-rank-math 슬러그의 활성 설치 수입니다. getgenie의 requires는 JSON false라서 워드프레스 버전 문자열처럼 비교하면 안 됩니다. 호스트 조건 검토에는 version, requires, requires_php를 쓰고, 설치 수와 평점은 조회 당시의 참고 지표로 같이 보관합니다.

인코딩 방식은 베이스 URL에 대괄호가 없고, 이번 네 슬러그도 –globoff 없이 encoded_exit 0으로 끝났습니다. 슬러그 인자만 바꿔 같은 필드 집합을 되풀이하니 호스트 조건을 나란히 놓기도 좋습니다. 인자가 길어진다는 점은 감수해야 하는데, 이번 스크립트는 –get으로 쿼리 문자열에 붙였습니다. name 필드에는 엔티티가 섞여 있어서 제품 제목은 readme 첫 줄 쪽이 더 읽기 쉽습니다.

1.0 JSON, 없는 슬러그, readme·PHP 헤더와 trunk가 어긋나는 곳

대괄호를 피하면서 버전만 보고 싶다면 info/1.0/ai-engine.json이 있습니다. 경로에 대괄호가 없으니 bad range가 날 조건 자체가 없습니다. 여기서는 1.2에 sections=0만 넣고 active_installs와 downloaded는 보내지 않은 호출도 함께 비교했습니다.

key_probe='import json,sys
d=json.load(sys.stdin)
keys=["slug","version","requires","requires_php","tested","last_updated","rating","num_ratings","active_installs","downloaded"]
for k in keys:
    present = "yes" if k in d else "no"
    print(k+" key="+present+" value="+json.dumps(d.get(k) if k in d else None, ensure_ascii=False))
'
curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 \
  "https://api.wordpress.org/plugins/info/1.0/ai-engine.json" \
  | python3 -c "$key_probe"
curl --globoff --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 \
  "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine&request[fields][sections]=0" \
  | python3 -c "$key_probe"
curl --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 \
  --write-out '\nhttp_code=%{http_code}\n' --output - \
  "https://api.wordpress.org/plugins/info/1.0/no-such-ai-plugin-xyz.json"

1.0 호출의 json10_exit는 0입니다. ai-engine의 version, requires, requires_php, tested는 1.2와 같은 3.8.4, 6.0, 8.1, 7.1.3입니다. 그런데 active_installs 키는 no, downloaded 키는 yes입니다. 설치 수가 필요하면 1.0만으로는 모자랍니다.

plugins_api() 레퍼런스는 active_installs를 활성 설치 수를 돌려줄지 정하는 bool로, 기본값은 false로 적어 둡니다. 이것은 PHP 함수 인자에 대한 설명입니다. 같은 페이지의 소스 조각은 요청 URL을 https://api.wordpress.org/plugins/info/1.2/ 로 두고 action과 request를 쿼리에 붙입니다. 이번 점검 스크립트의 HTTP 호출은 그 bool을 보내지 않고 sections=0만 넣었는데, json12_default_fields_exit는 0, active_installs 키는 yes, downloaded 키는 no였습니다. 해결 스크립트처럼 downloaded=1을 넣으면 downloaded 키가 생깁니다. PHP 기본값과 인자를 생략한 HTTP 응답을 같은 규칙으로 여기면, 필드가 빠졌다고 잘못 생각하기 쉽습니다.

없는 슬러그 no-such-ai-plugin-xyz로 보낸 재현 요청의 응답 본문은 {"error":"Plugin not found."}이고 http_code는 404입니다. 이 명령에는 –fail이 없어서 missing_exit는 0입니다. 슬러그가 디렉터리에 있는지는 종료 코드가 아니라 http_code와 error 본문으로 판단합니다. 해결 스크립트의 HEAD에서 네 플러그인 디렉터리 주소와 1.2 주소, plugins_api() 레퍼런스는 모두 200이었습니다.

다음은 API 응답과 readme, 메인 PHP 헤더를 차례로 맞대어 본 기록입니다. readme에서는 ai-engine, getgenie, jetpack, seo-by-rank-math의 제목, 요구 환경, Tested up to, Stable tag를 골라 읽었고, 메인 PHP에서는 같은 요구 환경과 Version을 찾았습니다. Jetpack은 trunk와 tags/16.3.1을 함께 조회해, 개발 중인 파일과 안정 배포 파일이 갈라지는 지점을 확인했습니다. readme.txt 핸드북 발췌는 필드를 해석한 근거를 같은 실행 기록에 남겨 두려고 넣은 단계입니다.

$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://api.wordpress.org/plugins/info/1.0/ai-engine.json" | python3 -c "$key_probe"
slug key=yes value="ai-engine"
version key=yes value="3.8.4"
requires key=yes value="6.0"
requires_php key=yes value="8.1"
tested key=yes value="7.1.3"
last_updated key=yes value="2026-10-08 6:29am GMT"
rating key=yes value=98
num_ratings key=yes value=870
active_installs key=no value=null
downloaded key=yes value=7886149
json10_exit:0
$ curl --globoff --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=ai-engine&request[fields][sections]=0" | python3 -c "$key_probe"
slug key=yes value="ai-engine"
version key=yes value="3.8.4"
requires key=yes value="6.0"
requires_php key=yes value="8.1"
tested key=yes value="7.1.3"
last_updated key=yes value="2026-10-08 6:29am GMT"
rating key=yes value=98
num_ratings key=yes value=870
active_installs key=yes value=90000
downloaded key=no value=null
json12_default_fields_exit:0
$ curl --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 --write-out "\nhttp_code=%{http_code}\n" --output - "https://api.wordpress.org/plugins/info/1.0/no-such-ai-plugin-xyz.json"
{"error":"Plugin not found."}
http_code=404
missing_exit:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://plugins.svn.wordpress.org/ai-engine/trunk/readme.txt" | grep -n -E '^(===|Requires at least:|Tested up to:|Requires PHP:|Stable tag:)'
1:=== AI Engine - The Chatbot, AI Framework & MCP for WordPress ===
5:Requires at least: 6.0
6:Tested up to: 7.1
7:Requires PHP: 8.1
8:Stable tag: 3.8.4
readme_exit:ai-engine:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://plugins.svn.wordpress.org/getgenie/trunk/readme.txt" | grep -n -E '^(===|Requires at least:|Tested up to:|Requires PHP:|Stable tag:)'
1:=== GetGenie – AI SEO Assistant & Content Writer with Keyword Research, AEO & GEO ===
4:Tested up to: 7.1
5:Stable tag: 4.5.6
6:Requires PHP: 7.4
readme_exit:getgenie:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://plugins.svn.wordpress.org/jetpack/trunk/readme.txt" | grep -n -E '^(===|Requires at least:|Tested up to:|Requires PHP:|Stable tag:)'
1:=== Jetpack - WP Security, Backup, Speed, & Growth ===
4:Stable tag: 16.3.1
5:Requires at least: 7.0
6:Requires PHP: 7.4
7:Tested up to: 7.1
readme_exit:jetpack:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 "https://plugins.svn.wordpress.org/seo-by-rank-math/trunk/readme.txt" | grep -n -E '^(===|Requires at least:|Tested up to:|Requires PHP:|Stable tag:)'
1:=== Rank Math SEO – AI SEO Tools to Dominate SEO Rankings ===
5:Tested up to: 7.1
6:Requires at least: 6.7
7:Requires PHP: 7.4
8:Stable tag: 1.0.280
readme_exit:seo-by-rank-math:0
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o "tmp/ai-engine.php" "https://plugins.svn.wordpress.org/ai-engine/trunk/ai-engine.php"
php_download_exit:ai-engine:0
$ grep -n -E 'Plugin Name:|Version:|Requires at least:|Requires PHP:|Tested up to:' "tmp/ai-engine.php"
4:Plugin Name: AI Engine
7:Version: 3.8.4
8:Requires at least: 6.0
9:Requires PHP: 8.1
php_header_done:ai-engine
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o "tmp/getgenie.php" "https://plugins.svn.wordpress.org/getgenie/trunk/getgenie.php"
php_download_exit:getgenie:0
$ grep -n -E 'Plugin Name:|Version:|Requires at least:|Requires PHP:|Tested up to:' "tmp/getgenie.php"
4: * Plugin Name: GetGenie AI
8: * Version: 4.5.6
php_header_done:getgenie
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o "tmp/rank-math.php" "https://plugins.svn.wordpress.org/seo-by-rank-math/trunk/rank-math.php"
php_download_exit:rank-math:0
$ grep -n -E 'Plugin Name:|Version:|Requires at least:|Requires PHP:|Tested up to:' "tmp/rank-math.php"
11: * Plugin Name:       Rank Math SEO
12: * Version:           1.0.280
php_header_done:rank-math
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o "tmp/jetpack-trunk.php" "https://plugins.svn.wordpress.org/jetpack/trunk/jetpack.php"
php_download_exit:jetpack-trunk:0
$ grep -n -E 'Plugin Name:|Version:|Requires at least:|Requires PHP:|Tested up to:' "tmp/jetpack-trunk.php"
3: * Plugin Name: Jetpack
7: * Version: 16.4-a.1
11: * Requires at least: 7.0
12: * Requires PHP: 7.4
php_header_done:jetpack-trunk
$ stable=$(curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 https://plugins.svn.wordpress.org/jetpack/trunk/readme.txt | sed -n "s/^Stable tag: *//p" | head -n 1); printf %s\\n "stable_tag=$stable"
stable_tag=16.3.1
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o tmp/jetpack-tag.php "https://plugins.svn.wordpress.org/jetpack/tags/16.3.1/jetpack.php"
jetpack_tag_download_exit:0
$ grep -n -E "Plugin Name:|Version:|Requires at least:|Requires PHP:|Tested up to:" tmp/jetpack-tag.php
3: * Plugin Name: Jetpack
7: * Version: 16.3.1
11: * Requires at least: 7.0
12: * Requires PHP: 7.4
jetpack_tag_php_done
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o tmp/readme-handbook.html https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/
handbook_exit:0
$ python3 -c "extract handbook windows"
HANDBOOK[Tested up to] s should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: tag1, tag2 Requires at least: 4.7 Tested up to: 5.4 Stable tag: 4.3 Requires PHP: 7.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Here is a short description of the plugin. This should be no more than 150 characters. No markup here. Contributors – a case sensitive, comma s
HANDBOOK[Tested up to] petitors plugin names as tags. Plugins should not use tags which are unique to that plugin, as these will not be shown. Tested up to – The version of WordPress that the plugin has been tested against. This field ignores minor versions, as plugins shouldn’t break with a minor update. This means a plugin only need to define the major version it is tested against and the WordPress.org plu
HANDBOOK[Requires PHP] the tool wpreadme.com Since WordPress 5.8 plugin readme files are not parsed for requirements . This means that headers Requires PHP and Requires at least are going to be parsed from plugin’s main PHP file. Section Details All plugins contain a main PHP file, and almost all plugins have a readme.txt file as well. The readme.txt file is intended to be written using a subset of markdown. Readm
HANDBOOK[Requires PHP] g userid's) Donate link: https://example.com/ Tags: tag1, tag2 Requires at least: 4.7 Tested up to: 5.4 Stable tag: 4.3 Requires PHP: 7.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Here is a short description of the plugin. This should be no more than 150 characters. No markup here. Contributors – a case sensitive, comma separated list of all WordPress.org
HANDBOOK[Requires at least] .com Since WordPress 5.8 plugin readme files are not parsed for requirements . This means that headers Requires PHP and Requires at least are going to be parsed from plugin’s main PHP file. Section Details All plugins contain a main PHP file, and almost all plugins have a readme.txt file as well. The readme.txt file is intended to be written using a subset of markdown. Readme Header Inf
HANDBOOK[Requires at least]  === Contributors: (this should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: tag1, tag2 Requires at least: 4.7 Tested up to: 5.4 Stable tag: 4.3 Requires PHP: 7.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Here is a short description of the plugin. This should be no more than 150 characters. No markup here. Contributors – a 
HANDBOOK[Stable tag]  of wordpress.org userid's) Donate link: https://example.com/ Tags: tag1, tag2 Requires at least: 4.7 Tested up to: 5.4 Stable tag: 4.3 Requires PHP: 7.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Here is a short description of the plugin. This should be no more than 150 characters. No markup here. Contributors – a case sensitive, comma separated list of a
HANDBOOK[Stable tag] needed for use with this plugin. This should be numbers  only , so ‘7.0’ and not ‘PHP 7.0’ Stable Tag – The stable version of the plugin. This is not the version of WordPress, but the version of the plugin itself. Only use numbers and periods, and SemVer formatting is recommended. License – The GPLV2 (or later) compatible license used for the plugin. Licens
HANDBOOK[minor version] not be shown. Tested up to – The version of WordPress that the plugin has been tested against. This field ignores minor versions, as plugins shouldn’t break with a minor update. This means a plugin only need to define the major version it is tested against and the WordPress.org plugin directory will automatically add the minor version. This should be numbers  only , so ‘4.9&
HANDBOOK[minor version] need to define the major version it is tested against and the WordPress.org plugin directory will automatically add the minor version. This should be numbers  only , so ‘4.9’ and not ‘WP 4.9’ Requires PHP – (OPTIONAL) The required minimum version of PHP needed for use with this plugin. This should be numbers  only , so ‘7.0’ and not ‘PHP 7.
$ curl --fail --silent --show-error --max-time 20 -A ainewsnote-guide/2026-10-14 -o tmp/plugins-api.html https://developer.wordpress.org/reference/functions/plugins_api/
plugins_api_doc_exit:0
$ python3 -c "extract plugins_api windows"
DOCIDX active_installs 4757
PLUGINS_API[active_installs] ugin reviews. Default false. banners bool Whether to return the banner images links. Default false. icons bool Whether to return the icon links. Default false. active_installs bool Whether to return the number of active installations. Default false. contributors bool Whether to return the list of contributors. Default false. Default: array() Return object|array| WP_Error Response object or array on success, WP_Error on failure. See the function reference article for more info
DOCIDX api.wordpress.org/plugins/info 6653
PLUGINS_API[api.wordpress.org/plugins/info] aram object $args Plugin API arguments. */ $res = apply_filters( 'plugins_api', false, $action, $args ); if ( false === $res ) { $url = 'https://api.wordpress.org/plugins/info/1.2/'; $url = add_query_arg( array( 'action' => $action, 'request' => $args, ), $url ); $http_url = $url; $ssl = wp_http_supports( array( 'ssl' ) ); if ( $ssl ) { $url = set_url_scheme( $url, 'https' ); } $http_args = array( 'timeout
DOCIDX plugin_information 1552
PLUGINS_API[plugin_information] ‘plugins_api’ , allows a plugin to override the WordPress.org Plugin Installation API entirely. If $action is ‘query_plugins’ or ‘plugin_information’, an object MUST be passed. If $action is ‘hot_tags’, an array MUST be passed. Finally, the third filter, ‘plugins_api_result’ , makes it possible to filter the response object or array, depending on the $action type. Supported arguments per action: Argument Name query_p
$ for u in ...; do curl -sI --max-time 15 -A ainewsnote-guide/2026-10-14 -o /dev/null -w "%{http_code} %{url_effective}\n" "$u"; done
200 https://api.wordpress.org/plugins/info/1.0/ai-engine.json
200 https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/
200 https://plugins.svn.wordpress.org/ai-engine/trunk/readme.txt
200 https://plugins.svn.wordpress.org/getgenie/trunk/readme.txt
200 https://plugins.svn.wordpress.org/jetpack/trunk/readme.txt
200 https://plugins.svn.wordpress.org/seo-by-rank-math/trunk/readme.txt
200 https://plugins.svn.wordpress.org/ai-engine/trunk/ai-engine.php
200 https://plugins.svn.wordpress.org/getgenie/trunk/getgenie.php
200 https://plugins.svn.wordpress.org/seo-by-rank-math/trunk/rank-math.php
200 https://plugins.svn.wordpress.org/jetpack/trunk/jetpack.php
200 https://plugins.svn.wordpress.org/jetpack/tags/16.3.1/jetpack.php

비교 출력에서 1.0의 active_installs 키는 no이고, sections=0만 지정한 1.2의 downloaded 키는 no입니다. 없는 슬러그는 http_code=404와 Plugin not found.를 돌려줬는데, –fail이 없는 명령의 missing_exit는 0으로 남았습니다. 종료 코드, HTTP 상태, JSON 내용이 저마다 다른 것을 말해 준다는 점이 아래 캡처에 대응하는 기록에서 드러납니다.

워드프레스 플러그인 API 실행 화면: plugins_api 문서의 active_installs 설명과 종료 값 0을 기록합니다
2026-10-14 14:09:43 KST, Debian 13 — plugins_api 문서의 active_installs 설명과 종료 값 0을 기록합니다.

API 필드와 헤더를 대조하고 문서의 active_installs 설명까지 뽑아낸 실행입니다. 문서 발췌에는 기본값이 false로 적혀 있고 출력에는 plugins_api_doc_exit:0이 남았습니다. 앞의 1.2 직접 호출에서는 그 인자를 생략해도 active_installs 키가 돌아왔으니, PHP 함수의 기본값과 HTTP 응답의 필드 구성은 별개입니다. 플러그인 이름도 슬러그 전체를 가리키므로, Jetpack의 설치 수를 특정 AI 기능만의 사용량으로 읽지 않습니다.

Tested up to는 네 readme 모두 7.1이고, API의 tested는 네 슬러그 모두 7.1.3입니다. 핸드북은 Tested up to가 시험한 워드프레스 버전이며 마이너 버전은 무시한다고 적습니다. 플러그인은 메이저 버전만 적어도 되고 디렉터리가 마이너 버전을 붙인다는 설명이 같은 문장에 있으며, 숫자만 써야 한다고도 적혀 있습니다. 두 문자열을 서로 다른 호환 범위로 취급하면 헤더와 API가 어긋난 것처럼 보입니다.

핸드북에 따르면 워드프레스 5.8부터는 readme를 요구 사항 파싱에 쓰지 않고, Requires PHP와 Requires at least를 메인 PHP 파일에서 읽습니다. Stable tag는 워드프레스 버전이 아니라 플러그인 자신의 안정 버전이고, 숫자와 마침표를 쓰며 SemVer를 권한다고 적습니다. 아래 비교에는 그 문장과 이번 grep이 실제로 맞춘 줄만 썼습니다.

ai-engine.php는 Requires at least 6.0, Requires PHP 8.1, Version 3.8.4로 API, readme와 같습니다. tags/16.3.1/jetpack.php는 Requires at least 7.0, Requires PHP 7.4, Version 16.3.1로 API와 같습니다. getgenie.php는 파일 전체에서 헤더 패턴에 걸린 줄이 Plugin Name과 Version 4.5.6뿐이고, readme에는 Requires at least 행 없이 Requires PHP 7.4만 있습니다. API의 requires는 JSON false, requires_php는 7.4입니다. rank-math.php도 Plugin Name과 Version 1.0.280만 걸렸고, readme에는 Requires at least 6.7과 Requires PHP 7.4가, API에는 requires 6.7과 requires_php 7.4가 있습니다. getgenie는 requires가 false여서 최소 워드프레스 버전을 이 필드로는 읽을 수 없습니다.

trunk/jetpack.php의 Version은 16.4-a.1입니다. trunk readme의 Stable tag는 16.3.1이고, 그 태그의 PHP Version도 16.3.1, API version도 16.3.1입니다. trunk의 16.4-a.1을 현재 배포 버전으로 적어 두면 1.2 응답과 맞지 않습니다. ai-engine, getgenie, seo-by-rank-math는 trunk PHP의 Version과 API version이 같았습니다. 점검 스크립트 끝의 HEAD에서는 readme, 메인 PHP, 1.0 JSON, 핸드북, jetpack 태그 파일이 모두 200이었습니다.

Jetpack은 trunk가 아니라 안정 태그로 배포판을 식별

Jetpack의 trunk PHP Version 16.4-a.1과 API version 16.3.1이 다르다는 것은, 버전 감시가 어느 경로를 읽어야 하는지를 알려 줍니다. 배포 후보를 확인하는 설정은 API version과 Stable tag를 기준으로 삼고, 파일 헤더를 대조할 때는 그 안정 태그 경로를 읽게 바꾸는 것이 맞습니다. trunk 경로만 박아 두면 개발 중인 버전을 운영 배포 버전으로 받아들이게 됩니다.

요구 환경을 모으는 경로도 손봐야 합니다. 출력에 들어 있는 핸드북은 워드프레스 5.8부터 요구 사항을 메인 PHP 헤더에서 읽는다고 하니, readme만 읽는 수집 설정은 API 응답과 안정 태그의 메인 PHP를 함께 대조하도록 고칩니다. readme의 Tested up to 7.1과 API tested 7.1.3은 마이너 버전 처리 설명에 맞춰 비교하고, 문자열이 다르다는 이유만으로 호환성 경보를 올리지 않습니다.

외부 API 조회를 헬스체크로 다시 쓸 생각이라면, 이번 404 응답에서도 missing_exit가 0이었다는 점부터 반영해야 합니다. 조회 작업은 HTTP 상태, JSON의 error, 필요한 키가 있는지를 함께 검사하게 합니다. Kubernetes probe는 애플리케이션 자체의 준비 상태만 보고, 외부 디렉터리 수집 상태는 따로 관찰하도록 나누는 구성을 권합니다. 그래야 외부 조회가 실패했을 때 서비스 프로세스 장애와 섞지 않고 대응할 수 있습니다.

주소 한 줄이면 –globoff, 여러 슬러그 비교면 –data-urlencode

요청 방식마다 종료 결과와 반환 키가 어떻게 달랐는지를 표로 묶었습니다. 대괄호 URL은 curl_exit:3으로 멈췄고, 인코딩한 요청은 슬러그마다 종료 코드 0으로 끝났습니다. 조회 도구를 정할 때는 주소 길이만 볼 것이 아니라, 뒤 단계에서 쓸 키가 응답에 있는지도 같이 봐야 합니다.

조회 방법이번 종료active_installs 키downloaded 키역할
대괄호 URL, –globoff 없음curl_exit:3응답 없음응답 없음bad range로 중단
1.2 –globoff, 필드 1globoff_exit:0yesyes주소 한 줄을 유지
1.2 –data-urlencode –get슬러그별 종료 코드 0yesyes슬러그 반복
1.2 sections=0만json12_default_fields_exit:0yesno다운로드 수를 빼는 짧은 조회
1.0 JSONjson10_exit:0noyes대괄호 없이 버전 확인

호스트 조건은 아래 표처럼 API에서 받은 버전과 요구 환경을 한 행에 놓고 비교합니다. Stable tag는 네 슬러그 모두 API version과 같았지만, Jetpack의 trunk PHP Version은 16.4-a.1이었습니다. getgenie의 false는 숫자 하한이 아니니 자동 비교에서 따로 처리해야 합니다.

슬러그versionrequiresrequires_phpAPI testedreadme Tested up totrunk PHP Version
ai-engine3.8.46.08.17.1.37.13.8.4
jetpack16.3.17.07.47.1.37.116.4-a.1
getgenie4.5.6false7.47.1.37.14.5.6
seo-by-rank-math1.0.2806.77.47.1.37.11.0.280

이미 주소 한 줄을 갖고 있다면 –globoff를 붙이는 쪽이 짧습니다. 여러 슬러그를 같은 필드로 비교할 때는 –data-urlencode를 권장합니다. 이번 네 슬러그가 바로 그 경우였고, –globoff 없이 종료 코드 0이었습니다. 대괄호를 피하면서 version과 requires만 보면 될 때는 1.0 JSON을 쓰고, 설치 수가 필요하면 active_installs 키가 있는 1.2로 돌아옵니다.

호스트 PHP가 8.1 미만이면 ai-engine의 requires_php 8.1에 맞지 않습니다. jetpack, getgenie, seo-by-rank-math의 requires_php는 7.4입니다. 코어 하한이 필드로 나오는 슬러그는 ai-engine 6.0, jetpack 7.0, seo-by-rank-math 6.7입니다. getgenie는 API requires가 false이고 메인 PHP와 readme 헤더에도 Requires at least가 없으니, 이 슬러그의 최소 워드프레스 버전은 이번 조회 필드로는 알 수 없습니다.

배포 버전은 API version으로 봅니다. jetpack만 trunk PHP Version이 16.4-a.1이고, API version과 Stable tag와 tags/16.3.1의 Version은 16.3.1입니다. readme의 7.1과 API의 7.1.3은 핸드북의 마이너 버전 설명에 비춰 읽고, 서로 다른 호환 범위로 세지 않습니다. jetpack은 보안·백업·속도가 제목인 슬러그 전체를 조회한 결과입니다.

네 슬러그의 조회 출력을 근거로, 여러 플러그인을 배포 검토에 올릴 때는 –get과 –data-urlencode를 묶은 1.2 조회 구성을 권합니다. 필요한 필드를 명시하고 응답 원본과 조회 시점을 함께 남겨 두면 다음 변경 때 요구 환경이 어떻게 달라졌는지 따라가기 쉽습니다. ai-engine을 고른다면 출력에 나온 PHP 8.1과 워드프레스 6.0을 호스트 조건과 먼저 맞춰 봅니다. 실시간 서비스에서는 외부 디렉터리 조회를 사용자 요청 처리와 떼어 두고, 저장해 둔 조회 결과를 검토 화면에서 읽게 하겠습니다. 이번 실험의 서버 자원 수치를 서비스 처리 용량으로 바꿔 읽기보다, 배포 후보의 호환 조건을 한결같이 모으는 도구로 이 스크립트를 쓰겠습니다.

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다