Judge0 1.13.1 REST API 使用详解

Judge0 1.13.1 HTTP 接口全量实测:/about、/languages、/statuses、/system_info、/config_info 只读端点,POST /submissions 明文与 base64 双模式、wait=true 同步语义、提交字段与限制上限、GET /submissions/{token} 与 fields 投影、batch 批量提交(上限 20)与批量查询(错误 token 返回 null)、callback_url 的 PUT + base64 载荷回调、鉴权头与 401 空响应体;附编译错误 UTF-8 序列化失败、additional_files 必须 zip、无提交列表接口、JUDGE0_VERSION 缺失启动失败等真实回显与踩坑。

Judge0Judge0代码评测REST APIRubyPostgreSQL2026/10/27

Judge0 1.13.1 REST API 使用详解

目标:把 Judge0 1.13.1 的 HTTP 接口按「只读元信息 → 提交 → 查询 → 回调」四条线拆开,给出可直接复制的 curl 命令与本地 1.13.1 实例的真实响应。文中覆盖 /about、/languages、/statuses、/system_info、/config_info、POST /submissions(明文与 base64 两种编码)、wait=true 同步语义、GET /submissions/{token}、/submissions/batch 批量提交与批量查询、鉴权头与 webhook 回调,并把实测中与直觉不符的行为(编译错误导致 UTF-8 序列化失败、additional_files 必须是 zip、没有提交列表接口、批量查询传错 token 返回 null)单独成节。

一、环境信息

1.1 实测实例

项值
Judge0 版本1.13.1
服务端实例 Ahttp://127.0.0.1:8138,完整语言运行时,未开启鉴权
服务端实例 Bhttp://127.0.0.1:8141,基线实例,/about 的 homepage 等字段为 null
鉴权实例 Chttp://127.0.0.1:8142,AUTHN_HEADER=X-Auth-Token、AUTHN_TOKEN=test_token_2026
语言数量47
数据库PostgreSQL 14.24,judge0 库
队列Redis 6.0.16,队列键 resque:queue:1.13.1
请求工具curl 7.81.0、Python 3.10 标准库 urllib

三个实例共用同一份代码与数据库,差别只在环境变量,因此同一份请求在不同实例上的行为可以直接对比。

1.2 客户端约定

  • 所有请求与响应都是 application/json,字符集 UTF-8;请求必须带 Content-Type: application/json,否则 POST 会被当成表单解析并返回 400。
  • 时间单位是秒,time 字段是字符串(保留三位小数的 CPU 时间),memory 是整数(KB)。
  • 未特别说明时,base64_encoded 省略等价于 false:不带该参数提交明文源码可以正常执行,查询返回的 stdout 也是明文。这一点容易和官方文档里「默认 true」的旧描述混淆,以实测为准。
  • 下文 curl 一律加 -s 以便直接贴出响应体;真实使用时建议加 -w '\n[%{http_code}]\n' 观察状态码。

二、API 基础

2.1 基址

没有 /api/v1 之类的前缀,全部端点都挂在根路径上:

BASE=http://127.0.0.1:8138

判断对端是不是 Judge0、版本是多少,先打 /about。

2.2 内容类型与字符编码

source_code、stdin、expected_output、command_line_arguments、compiler_options、additional_files、callback_url 都是字符串字段。当源码或输入包含换行、引号、中文时,直接嵌进 JSON 最省事;当源码本身是二进制、或响应里的 stderr / compile_output 含非 UTF-8 字节时,改用 base64。

base64_encoded=true 是双向开关:请求体里的上述字符串按 base64 解码后再执行,响应里的 stdout / stderr / compile_output / message 也按 base64 编码返回。开关必须放在 query string 上,放在 JSON body 里无效——把 base64 源码和 "base64_encoded": true 一起放进 body,服务端会把源码原样当文本执行:

$ curl -s -X POST "$BASE/submissions?wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"cHJpbnQoImJvZHlmbGFnIik=","language_id":71,"base64_encoded":true}'
{"stdout":null,"time":"0.01","memory":7876,"stderr":"  File \"script.py\", line 1\n    cHJpbnQoImJvZHlmbGFnIik=\n                           ^\nSyntaxError: invalid syntax\n","token":"7f8f595c-360a-46ef-94aa-c2f537180666","compile_output":null,"message":"Exited with error status 1","status":{"id":11,"description":"Runtime Error (NZEC)"}}

错误信息里引用的正是那串 base64 文本,可直接确认开关没有生效。

2.3 鉴权

Judge0 的鉴权是「共享密钥头」模型,没有用户体系。服务端设 AUTHN_HEADER 与 AUTHN_TOKEN 后,客户端把 token 放进指定名字的请求头,或同名 query 参数。

未配置鉴权时(实例 A):

$ curl -s -w " [%{http_code}]" $BASE/about
{"version":"1.13.1","homepage":"https://judge0.com","source_code":"https://github.com/judge0/judge0","maintainer":"Herman Zvonimir Došilović \u003chermanz.dosilovic@gmail.com\u003e"} [200]

配置鉴权后(实例 C),无凭据的请求直接 401 且响应体为空:

$ curl -s -w " [%{http_code}]" http://127.0.0.1:8142/about
 [401]

$ curl -s -w " [%{http_code}]" -H "X-Auth-Token: test_token_2026" http://127.0.0.1:8142/about
{"version":"1.13.1","homepage":null,"source_code":null,"maintainer":null} [200]

$ curl -s -w " [%{http_code}]" "http://127.0.0.1:8142/about?X-Auth-Token=test_token_2026"
{"version":"1.13.1","homepage":null,"source_code":null,"maintainer":null} [200]

401 没有 body,客户端不能靠解析 JSON 判断失败原因,必须看 HTTP 状态码。请求头与 query 参数两条路等价;query 参数会出现在访问日志和浏览器历史里,生产环境只用请求头。

2.4 端点速查

方法路径用途鉴权
GET/about版本与项目元信息同全局
GET/languages语言清单(id、名称)同全局
GET/statuses14 种提交状态字典同全局
GET/system_info执行节点的 CPU/内存信息同全局
GET/config_info当前生效的配置快照同全局
POST/submissions单条提交同全局
GET/submissions/{token}按 token 查单条同全局
POST/submissions/batch批量提交(上限 20)同全局
GET/submissions/batch?tokens=...批量查询同全局

没有「列出全部提交」的接口:GET /submissions 返回 403 且响应体为空,见 8.4。所有查询都必须凭提交时拿到的 token。

三、只读端点

3.1 /about

$ curl -s $BASE/about | python3 -m json.tool
{
    "version": "1.13.1",
    "homepage": "https://judge0.com",
    "source_code": "https://github.com/judge0/judge0",
    "maintainer": "Herman Zvonimir Došilović <hermanz.dosilovic@gmail.com>"
}

version 来自编译进应用的常量,不随部署方式变化。homepage / source_code / maintainer 在实例 A 有值,在实例 B 为 null——后者说明该实例没有注入 JUDGE0_* 环境变量,仅影响展示,不影响提交。

3.2 /languages

返回数组,元素只有 id 与 name:

$ curl -s $BASE/languages | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d)); print(d[:2])"
47
[{'id': 45, 'name': 'Assembly (NASM 2.14.02)'}, {'id': 46, 'name': 'Bash (5.0.0)'}]

常用的几个:

$ curl -s $BASE/languages | python3 -c "import sys,json; print([x for x in json.load(sys.stdin) if x['id'] in (54,62,63,71)])"
[{'id': 54, 'name': 'C++ (GCC 9.2.0)'}, {'id': 62, 'name': 'Java (OpenJDK 13.0.1)'}, {'id': 63, 'name': 'JavaScript (Node.js 12.14.0)'}, {'id': 71, 'name': 'Python (3.8.1)'}]

名称里带运行时版本号,便于判断升级后语言是否变化。清单里存在但实际无法运行的条目,通常是运行时二进制缺失,特征见 8.7。

3.3 /statuses

$ curl -s $BASE/statuses
[{"id":1,"description":"In Queue"},{"id":2,"description":"Processing"},{"id":3,"description":"Accepted"},{"id":4,"description":"Wrong Answer"},{"id":5,"description":"Time Limit Exceeded"},{"id":6,"description":"Compilation Error"},{"id":7,"description":"Runtime Error (SIGSEGV)"},{"id":8,"description":"Runtime Error (SIGXFSZ)"},{"id":9,"description":"Runtime Error (SIGFPE)"},{"id":10,"description":"Runtime Error (SIGABRT)"},{"id":11,"description":"Runtime Error (NZEC)"},{"id":12,"description":"Runtime Error (Other)"},{"id":13,"description":"Internal Error"},{"id":14,"description":"Exec Format Error"}]

1、2 是过程态,3 之后是终态。客户端轮询时应只把 status.id > 2 当作结束条件;把 1 当失败会误杀正常排队的任务。

3.4 /system_info

返回的是 lscpu 文本的结构化结果,字段名原样保留(含空格),不是驼峰命名:

$ curl -s $BASE/system_info | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d)); print({k:d[k] for k in list(d)[:8]})"
44
{'Architecture': 'x86_64', 'CPU op-mode(s)': '32-bit, 64-bit', 'Byte Order': 'Little Endian', 'Address sizes': '45 bits physical, 48 bits virtual', 'CPU(s)': '6', 'On-line CPU(s) list': '0-5', 'Thread(s) per core': '1', 'Core(s) per socket': '6'}

末尾两个字段是内存:

$ curl -s $BASE/system_info | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Mem'], d['Swap'])"
18Gi 8.0Gi

这个端点不需要鉴权以外的前置条件,适合做「执行节点还活着吗」的探针;但它只反映宿主机,不反映队列积压。

3.5 /config_info

把当前生效的限制与开关一次性列出,共 39 个键,是排查「为什么我的限制参数被拒」的最快入口:

$ curl -s $BASE/config_info
{"maintenance_mode":false,"enable_wait_result":true,"enable_compiler_options":true,"allowed_languages_for_compile_options":[],"enable_command_line_arguments":true,"enable_submission_delete":false,"enable_callbacks":true,"callbacks_max_tries":3,"callbacks_timeout":5.0,"enable_additional_files":true,"max_queue_size":100,"cpu_time_limit":5.0,"max_cpu_time_limit":15.0,"cpu_extra_time":1.0,"max_cpu_extra_time":5.0,"wall_time_limit":10.0,"max_wall_time_limit":20.0,"memory_limit":128000,"max_memory_limit":512000,"stack_limit":64000,"max_stack_limit":128000,"max_processes_and_or_threads":60,"max_max_processes_and_or_threads":120,"enable_per_process_and_thread_time_limit":true,"allow_enable_per_process_and_thread_time_limit":true,"enable_per_process_and_thread_memory_limit":true,"allow_enable_per_process_and_thread_memory_limit":true,"max_file_size":1024,"max_max_file_size":4096,"number_of_runs":1,"max_number_of_runs":20,"redirect_stderr_to_stdout":false,"max_extract_size":10240,"enable_batched_submissions":true,"max_submission_batch_size":20,"submission_cache_duration":1.0,"use_docs_as_homepage":false,"allow_enable_network":true,"enable_network":false}

注意 max_* 系列是「客户端可申请的上限」,不是「当前使用值」:cpu_time_limit 5.0 是默认值,max_cpu_time_limit 15.0 才是单次提交能申请到的天花板。各字段的含义与调整方式见同专栏《Judge0 1.13.1 配置参数详解》。

四、提交

4.1 最小提交(明文)

$ curl -s -X POST "$BASE/submissions?base64_encoded=false&wait=true" \
    -H "Content-Type: application/json" \
    -d '{"source_code":"print(\"hello judge0\")","language_id":71}' | python3 -m json.tool
{
    "stdout": "hello judge0\n",
    "time": "0.019",
    "memory": 7944,
    "stderr": null,
    "token": "4a7b4730-bbae-4ea1-8540-a139117135f7",
    "compile_output": null,
    "message": null,
    "status": {
        "id": 3,
        "description": "Accepted"
    }
}

token 是 UUID v4,是后续查询与回调的唯一凭据,必须落库。time 与 memory 在同步提交里由执行过程回填;异步提交刚创建时这两个字段是 null。

4.2 base64 提交与响应

$ curl -s -X POST "$BASE/submissions?base64_encoded=true&wait=true" \
    -H "Content-Type: application/json" \
    -d '{"source_code":"cHJpbnQoImJhc2U2NCBoZWxsbyIp","language_id":71}'
{"stdout":"YmFzZTY0IGhlbGxvCg==\n","time":"0.009","memory":7900,"stderr":null,"token":"b3e7b485-d212-4fc9-84e0-e4a5afba60a9","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

响应里的 stdout 也是 base64,解码即 base64 hello:

$ echo "YmFzZTY0IGhlbGxvCg==" | base64 -d
base64 hello

查询时同样要带 base64_encoded=true 才会拿到 base64 形态:

$ curl -s "$BASE/submissions/b3e7b485-d212-4fc9-84e0-e4a5afba60a9?base64_encoded=true"
{"stdout":"YmFzZTY0IGhlbGxvCg==\n","time":"0.009","memory":7900,"stderr":null,"token":"b3e7b485-d212-4fc9-84e0-e4a5afba60a9","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

不带该参数则返回明文——base64_encoded 只控制当前这次请求的编解码,不会改变已存数据的存储形态。

4.3 wait=true 的语义

wait=true 让提交在 API 进程内同步执行完再返回,不经过队列,因此响应里直接带 stdout 和终态。它有三个直接后果:

  1. 请求耗时等于编译加执行耗时,客户端超时必须放宽到 wall_time_limit 之上;
  2. 不占用队列,但占用一个 API 线程(Puma 线程数由 RAILS_MAX_THREADS 决定);
  3. 服务端关闭 ENABLE_WAIT_RESULT 后,带 wait=true 的请求会被拒绝:
$ curl -s -w " [%{http_code}]" -X POST "http://127.0.0.1:8142/submissions?wait=true" \
    -H "Content-Type: application/json" \
    -d '{"source_code":"print(1)","language_id":71}'
{"error":"wait not allowed"} [400]

不带 wait 时返回 201 与 token,立即结束:

$ curl -s -w " [%{http_code}]" -X POST "$BASE/submissions?base64_encoded=false" \
    -H "Content-Type: application/json" \
    -d '{"source_code":"print(123)","language_id":71}'
{"token":"f69c649f-e710-44f0-b1c6-e557ce27fcba"} [201]

4.4 提交字段与实测边界

字段类型默认实测说明
source_codestring必填空白时 422
language_idint必填不存在时 422
stdinstring空作为进程标准输入
expected_outputstring空设置后才会比对,不一致判 Wrong Answer
additional_filesstring空必须是 base64 编码的 zip,见 4.7
command_line_argumentsstring空按空格切分传给程序
compiler_optionsstring空受 enable_compiler_options 控制
callback_urlstring空终态后回调,见第五章
cpu_time_limitfloat5.0上限 15.0,超出 422
wall_time_limitfloat10.0上限 20.0
memory_limitint128000单位 KB,上限 512000
stack_limitint64000上限 128000
max_processes_and_or_threadsint60上限 120
number_of_runsint1多次运行取最慢一次

带 expected_output 的判定实测:

$ curl -s -X POST "$BASE/submissions?wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"a,b=map(int,input().split())\nprint(a+b)","language_id":71,"stdin":"2 3","expected_output":"6"}'
{"stdout":"5\n","time":"0.009","memory":7976,"stderr":null,"token":"5fbb384a-70df-46ad-a512-1e06c936dfcb","compile_output":null,"message":null,"status":{"id":4,"description":"Wrong Answer"}}

只把 expected_output 改成 5,同一份源码即为 Accepted——输出比对按去除行尾空白后的精确匹配,不做数值归一化。

command_line_arguments 实测:

$ curl -s -X POST "$BASE/submissions?wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"import sys\nprint(sys.argv[1:])","language_id":71,"command_line_arguments":"-x 7"}'
{"stdout":"['-x', '7']\n","time":"0.009","memory":7996,"stderr":null,"token":"20090cd9-7ad3-40ec-aea2-f2d9f2d2f881","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

4.5 结果字段的含义

字段含义
stdout标准输出,含原始换行
stderr标准错误,运行时异常栈在这里
compile_output编译器输出,仅编译型语言有值
message执行器附加信息,如 Time limit exceeded (wall clock)、Exited with error status 1
status{id, description},取值见 /statuses
timeCPU 时间(秒,字符串);wait 同步执行时可能是墙钟时间,见 8.8
memory峰值内存(KB,整数)

4.6 单条查询

$ curl -s "$BASE/submissions/4a7b4730-bbae-4ea1-8540-a139117135f7"
{"stdout":"hello judge0\n","time":"0.019","memory":7944,"stderr":null,"token":"4a7b4730-bbae-4ea1-8540-a139117135f7","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

fields 可以只取需要的键,减少传输量:

$ curl -s "$BASE/submissions/b3e7b485-d212-4fc9-84e0-e4a5afba60a9?fields=stdout,status,time,memory"
{"stdout":"base64 hello\n","time":"0.009","memory":7900,"status":{"id":3,"description":"Accepted"}}

fields 里没列出的键不会出现,token 也不会被隐式带回;需要 token 时显式写进 fields。

不存在的 token 返回 404 且响应体为空:

$ curl -s -o /dev/null -w "%{http_code} %{size_download}\n" "$BASE/submissions/not-a-token"
404 0

4.7 additional_files 必须是 zip

官方文档对 additional_files 的描述是「base64 编码的 zip 压缩包」,实测传单个文件的 base64 不会解压,程序按相对路径找不到文件:

$ curl -s -X POST "$BASE/submissions?base64_encoded=true&wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"cHJpbnQob3BlbigiZGF0YS50eHQiKS5yZWFkKCkp","language_id":71,"additional_files":"NDIK"}'
{"stdout":null,"stderr":"VHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOgogIEZpbGUgInNj\ncmlwdC5weSIsIGxpbmUgMSwgaW4gPG1vZHVsZT4KICAgIHByaW50KG9wZW4o\nJ2RhdGEudHh0JykucmVhZCgpLnN0cmlwKCkpCkZpbGVOb3RGb3VuZEVycm9y\nOiBbRXJybm8gMl0gTm8gc3VjaCBmaWxlIG9yIGRpcmVjdG9yeTogJ2RhdGEu\ndHh0Jwo=\n","message":"RXhpdGVkIHdpdGggZXJyb3Igc3RhdHVzIDE=\n","token":"99d840c8-da4f-4066-9656-f1f5fa707fe0","compile_output":null,"status":{"id":11,"description":"Runtime Error (NZEC)"}}

解码后的 stderr 是:

Traceback (most recent call last):
  File "script.py", line 1, in <module>
    print(open('data.txt').read().strip())
FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'

改成真正的 zip(内含 data.txt)后通过:

$ python3 -c "import io,zipfile,base64; b=io.BytesIO(); z=zipfile.ZipFile(b,'w'); z.writestr('data.txt','42\n'); z.close(); print(base64.b64encode(b.getvalue()).decode())"
UEsDBBQAAAAAAFa5W10xKYbRAwAAAAMAAAAIAAAAZGF0YS50eHQ0MgpQSwECFAMUAAAAAABWuVtdMSmG0QMAAAADAAAACAAAAAAAAAAAAAAAgAEAAAAAZGF0YS50eHRQSwUGAAAAAAEAAQA2AAAAKQAAAAAA
$ curl -s -X POST "$BASE/submissions?base64_encoded=true&wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"cHJpbnQob3BlbigiZGF0YS50eHQiKS5yZWFkKCkuc3RyaXAoKSk=","language_id":71,"additional_files":"UEsDBBQAAAAAAFa5W10xKYbRAwAAAAMAAAAIAAAAZGF0YS50eHQ0MgpQSwECFAMUAAAAAABWuVtdMSmG0QMAAAADAAAACAAAAAAAAAAAAAAAgAEAAAAAZGF0YS50eHRQSwUGAAAAAAEAAQA2AAAAKQAAAAAA"}'
{"stdout":"NDIK\n","time":"0.01","memory":7884,"stderr":null,"token":"dd3855ca-2e98-4cd1-9274-4726ffc3ac0c","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

NDIK 解码为 42\n,说明 zip 已解到工作目录。压缩包只能包含文件本身,目录层级会被一并展开到工作目录。

4.8 批量提交

$ curl -s -X POST "$BASE/submissions/batch?base64_encoded=false" -H "Content-Type: application/json" \
    -d '{"submissions":[{"source_code":"print(\"b1\")","language_id":71},{"source_code":"print(\"b2\")","language_id":71},{"source_code":"print(1/0)","language_id":71}]}'
[{"token":"bcedc899-a3d0-4502-bd97-61b64f412a09"},{"token":"f3a9d26f-bd4a-4427-8685-468fead6f938"},{"token":"cb2c127d-2189-4d89-a868-e11a65f90982"}]

批量提交是异步的:响应只有 token,没有执行结果,即使服务端开了 ENABLE_WAIT_RESULT 也不支持 wait=true。批量上限 20,把上面三条扩成 21 条(同样的提交对象重复 21 次)后直接拒绝:

$ python3 -c "
import json, urllib.request, urllib.error
subs = {'submissions': [{'source_code': 'print(1)', 'language_id': 71} for _ in range(21)]}
r = urllib.request.Request('$BASE/submissions/batch?base64_encoded=false',
                           data=json.dumps(subs).encode(),
                           headers={'Content-Type': 'application/json'}, method='POST')
try:
    urllib.request.urlopen(r)
except urllib.error.HTTPError as e:
    print(e.code, e.read().decode())
"
400 {"error":"number of submissions in a batch should be less than or equal to 20"}

4.9 批量查询

$ curl -s "$BASE/submissions/batch?tokens=bcedc899-a3d0-4502-bd97-61b64f412a09,f3a9d26f-bd4a-4427-8685-468fead6f938,cb2c127d-2189-4d89-a868-e11a65f90982&fields=stdout,status"
{"submissions":[{"stdout":"b1\n","status":{"id":3,"description":"Accepted"}},{"stdout":"b2\n","status":{"id":3,"description":"Accepted"}},{"stdout":null,"status":{"id":11,"description":"Runtime Error (NZEC)"}}]}

返回顺序与 tokens 顺序一致,长度也一致;不存在的 token 不会让整个请求失败,而是占位为 null:

$ curl -s -w " [%{http_code}]" "$BASE/submissions/batch?tokens=not-a-token"
{"submissions":[null]} [200]

这意味着批量查询不能用「数组里有 null」判断 token 拼写错误,只能逐项判空。tokens 为空则 400:

$ curl -s -w " [%{http_code}]" "$BASE/submissions/batch?tokens="
{"error":"there should be at least one submission in a batch"} [400]

五、回调(webhook)

5.1 注册回调

在提交体里带 callback_url 即可,回调与 wait 互斥使用——异步提交 + 回调是生产环境的常规组合:

$ curl -s -X POST "$BASE/submissions" -H "Content-Type: application/json" \
    -d '{"source_code":"print(7*8)","language_id":71,"callback_url":"http://<回调主机>:8199/cb"}'
{"token":"139a83a8-b6d6-42e2-b2d3-ffde1ba72461"}

5.2 回调报文

终态后接收端收到的是 PUT,不是 POST——接收器只实现 do_POST 会静默收不到任何东西:

METHOD=PUT PATH=/cb
{"stdout":"NTYK\n","time":"0.009","memory":7992,"stderr":null,"token":"823ac49f-497e-4dcb-bbc8-07a24514d6f7","compile_output":null,"message":null,"status":{"id":3,"description":"Accepted"}}

报文体是整个提交对象的 base64 编码 JSON,需要先 base64 解码再解析,且解码后各字段仍是 base64(NTYK 解码为 56\n)。接收端要按「解一层 base64 → 解析 JSON → 再解字段」的顺序处理。

5.3 重试与超时

重试次数与超时来自配置项 CALLBACKS_MAX_TRIES(当前 3)与 CALLBACKS_TIMEOUT(当前 5.0 秒),可在 /config_info 的 callbacks_max_tries / callbacks_timeout 读到。接收端 5 秒内不响应会被判定失败并重试;重试不改变提交状态,只是补投。回调失败不会让提交变成 Internal Error,因此不能靠提交状态反推回调是否送达,落库时要自己记录「回调已确认」。

六、错误与状态码

6.1 HTTP 状态码分布

码场景响应体
200查询成功JSON
201创建提交成功(含异步与 wait=true)JSON
400wait 被禁用、批量超限、批量参数缺失、JSON 解析失败含 error 字段的 JSON
401鉴权失败空
403访问不存在的集合端点(如 GET /submissions)空
404token 不存在空
422字段校验失败(语言不存在、限制超上限)字段名到错误数组的映射
500服务端未捕获异常(如维护模式下非 ASCII 提示语的编码错误)空或 HTML

6.2 422 校验错误

$ curl -s -w " [%{http_code}]" -X POST "$BASE/submissions" -H "Content-Type: application/json" -d '{"source_code":"print(1)","language_id":9999}'
{"language_id":["language with id 9999 doesn't exist"]} [422]

$ curl -s -w " [%{http_code}]" -X POST "$BASE/submissions" -H "Content-Type: application/json" -d '{"source_code":"print(1)"}'
{"language_id":["can't be blank","language with id  doesn't exist"]} [422]

$ curl -s -w " [%{http_code}]" -X POST "$BASE/submissions?wait=true" -H "Content-Type: application/json" -d '{"source_code":"print(1)","language_id":71,"memory_limit":600000}'
{"memory_limit":["must be less than or equal to 512000"]} [422]

键是字段名,值是错误消息数组,适合直接回显给前端。注意缺 language_id 时会同时返回两条消息,第二条里 id 后面是两个空格再跟 doesn't exist,这是服务端字符串插值为空导致的,不要把它当稳定文案。

6.3 201 但带 error 字段

编译型语言报错时,POST 可能返回 201 且带 token,但同时在顶层给出 error:

$ curl -s -X POST "$BASE/submissions?wait=true" -H "Content-Type: application/json" -d '{"source_code":"int main(){ oops }","language_id":54}'
{"error":"some attributes for this submission cannot be converted to UTF-8, use base64_encoded=true query parameter","token":"addc2afd-7341-4dbf-8cbd-7382a98e6467"}

提交其实已经创建成功(可用 token 查询),只是编译器输出里有非 UTF-8 字节,服务端拒绝序列化。用 base64_encoded=true 重提即可拿到完整编译错误:

$ curl -s -X POST "$BASE/submissions?base64_encoded=true&wait=true" -H "Content-Type: application/json" \
    -d '{"source_code":"aW50IG1haW4oKXsgd3JvbmcgfQ==","language_id":54}'
{"stdout":null,"time":null,"memory":null,"stderr":null,"token":"45c91827-c47f-43ce-a159-c21901a51a8e","compile_output":"bWFpbi5jcHA6IEluIGZ1bmN0aW9uIOKAmGludCBtYWluKCnigJk6Cm1haW4u\nY3BwOjE6MTM6IGVycm9yOiDigJh3cm9uZ+KAmSB3YXMgbm90IGRlY2xhcmVk\nIGluIHRoaXMgc2NvcGUKICAgIDEgfCBpbnQgbWFpbigpeyB3cm9uZyB9CiAg\nICAgIHwgICAgICAgICAgICAgXn5+fn4K\n","message":null,"status":{"id":6,"description":"Compilation Error"}}

解码后的 compile_output:

main.cpp: In function ‘int main()’:
main.cpp:1:13: error: ‘wrong’ was not declared in this scope
    1 | int main(){ wrong }
      |             ^~~~~

结论:接入编译型语言时,提交端默认就用 base64_encoded=true,否则任何一个非 ASCII 的编译诊断都会让响应变成不含结果的错误包。

七、客户端实践

7.1 curl 全流程

BASE=http://127.0.0.1:8138
T=$(curl -s -X POST "$BASE/submissions?base64_encoded=false" -H "Content-Type: application/json" \
      -d '{"source_code":"print(6*7)","language_id":71}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
echo "$T"

for i in $(seq 1 30); do
  R=$(curl -s "$BASE/submissions/$T?fields=status,stdout,time,memory")
  ID=$(echo "$R" | python3 -c "import sys,json;print(json.load(sys.stdin)['status']['id'])")
  echo "$R"
  [ "$ID" -gt 2 ] && break
  sleep 0.5
done

轮询间隔不必太小:队列键里的任务由消费者进程按 INTERVAL(默认 0.1 秒)取,消费者空闲时提交到开始执行通常在一秒内,见 7.2。

7.2 异步状态迁移

$ T=$(curl -s -X POST "$BASE/submissions?base64_encoded=false" -H "Content-Type: application/json" \
      -d '{"source_code":"import time\ntime.sleep(3)\nprint(9)","language_id":71}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")

$ for i in 1 2 3 4 5; do curl -s "$BASE/submissions/$T?fields=status,time,memory"; echo; sleep 1; done
{"time":null,"memory":null,"status":{"id":1,"description":"In Queue"}}
{"time":null,"memory":null,"status":{"id":2,"description":"Processing"}}
{"time":null,"memory":null,"status":{"id":2,"description":"Processing"}}
{"time":null,"memory":null,"status":{"id":2,"description":"Processing"}}
{"time":"0.01","memory":7796,"status":{"id":3,"description":"Accepted"}}

Processing 期间 time 与 memory 仍为 null,不要让前端在读不到数值时显示 0。

7.3 同步与异步的取舍

维度wait=true异步 + 轮询或回调
延迟一次请求拿到结果至少两次网络往返
连接占用占一个 API 线程直到执行完立即释放
队列不经过队列受 MAX_QUEUE_SIZE 约束
适用交互式试跑、结果必然很短批量判题、长任务、可重试的离线任务
依赖开关需要 ENABLE_WAIT_RESULT=true无

wait=true 在 API 进程内联执行,因此它不受队列上限保护:队列满时异步提交返回 503,而同步提交照样能进——这既是优点(不受积压影响),也是风险(大量同步请求会把 API 线程耗尽,进而拖垮队列消费的入队接口)。相关限制项与生产取值见同专栏《Judge0 1.13.1 配置参数详解》。

八、常见问题

8.1 提交返回 wait not allowed

服务端 ENABLE_WAIT_RESULT=false。去掉 wait=true 改为轮询,或让运维打开开关(改环境变量后重启 API 进程)。

8.2 编译错误时响应里没有 compile_output

见 6.3。编译器诊断含非 UTF-8 字节,服务端无法序列化。始终使用 base64_encoded=true 提交与查询。

8.3 additional_files 解压后文件不存在

传的是单个文件的 base64,而该字段只接受 zip。用 zipfile 打包后再 base64,见 4.7。若 zip 里带目录层级,解压后路径也会带层级,程序里的相对路径要跟着调整。

8.4 想列历史提交却拿到 403

Judge0 不提供提交列表接口。GET /submissions 返回 403 且 body 为空,GET /submissions/batch 必须显式带 tokens。要检索历史,只能自己在业务侧按 token 建档。

8.5 401 看不到错误信息

鉴权失败时响应体为空,状态码是唯一信号。检查三件事:请求头名是否与服务端 AUTHN_HEADER 完全一致(大小写不敏感但拼写必须对)、token 值是否与服务端 AUTHN_TOKEN 一致、是否只配了 token 而没配 header(此时服务端要求所有请求带一个空名字的请求头,实际效果是全部 401)。

8.6 服务端启动即退出,API 全部不可用

isolate_job.rb 顶部用 queue_as ENV["JUDGE0_VERSION"].to_sym 决定队列名,未设置 JUDGE0_VERSION 时抛异常并终止进程:

/opt/judge0/app/jobs/isolate_job.rb:4:in `<class:IsolateJob>': undefined method `to_sym' for nil:NilClass (NoMethodError)

手工启动 API 进程(不经 systemd 单元)时必须自己补 JUDGE0_VERSION,否则表现为端口一直连不上而不是返回 5xx。

8.7 语言在清单里但执行即 Internal Error

/languages 的 47 条来自数据库种子,不校验运行时是否真的存在。请求里硬编码了绝对路径的语言(如某些 Python 条目指向 /usr/local/python-3.8.1/bin/python3),在该路径不存在时返回:

run: line 1: /usr/local/python-3.8.1/bin/python3: No such file or directory

提交结果里 status 为 13 Internal Error、message 为 Exited with error status 127。排查方式是在执行节点上直接 ls 该路径;在还没装齐运行时的节点上做冒烟测试,应选 Plain Text(language_id=43)或系统自带路径的语言。

8.8 time 到底是 CPU 时间还是墙钟

time 字段在正常执行时是 CPU 时间;CPU 时间没超限而墙钟超限时,message 会明确标注:

{"stdout":null,"time":"0.002","memory":3160,"message":"Time limit exceeded (wall clock)","status":{"id":5,"description":"Time Limit Exceeded"}}

因此看到 status.id=5 但 time 远小于 cpu_time_limit,应去比对 wall_time_limit,而不是怀疑计时器坏了。内存超限不会给出独立状态码,通常表现为语言层抛错并落到 Runtime Error (NZEC):

{"stdout":null,"stderr":"Traceback (most recent call last):\n  File \"script.py\", line 1, in \u003cmodule\u003e\n    a='x'*(300*1024*1024)\nMemoryError\n","message":"Exited with error status 1","status":{"id":11,"description":"Runtime Error (NZEC)"}}

九、参考链接