Repository navigation
DictQuery returns 404 for every word: EDRDG JMdictDB is now behind a site-wide password gate #54
Description
Re-diagnosed 2026-09-14. The original root cause (
name="e"→name="g"markup change) is superseded — see Superseded diagnosis at the bottom for the original text. The endpoint is still broken, but for a different and more fundamental reason, and the fix proposed in the first version of this issue would not have worked.
Summary
POST /api/DictQuery/ returns 404 No results found for every word. The cause is not a markup change: the entire https://www.edrdg.org/jmwsgi/ tree (JMdictDB) is now behind a site-wide password gate, so the scraper never reaches a results page at all.
The gate returns HTTP 200 with an access-request page, so httpx raises nothing, BeautifulSoup finds 0 rows, and api/dict_query.py:125-130 emits its own 404. The endpoint reports "no results" when the real condition is "access denied".
Evidence
Probed 2026-09-14. Every /jmwsgi/ path returns the same JMdictDB - Site Access page:
| URL probed | HTTP | tr.resrow |
name="e" |
name="g" |
title |
|---|---|---|---|---|---|
srchres.py?... (the exact URL built at api/dict_query.py:44) |
200 | 0 | 0 | 0 | JMdictDB - Site Access |
entr.py?svc=jmdict&e=2313503 (old form) |
200 | 0 | 0 | 0 | JMdictDB - Site Access |
entr.py?svc=jmdict&g=2727400.1~2313503 (new form) |
200 | 0 | 0 | 0 | JMdictDB - Site Access |
srchform.py?svc=jmdict |
200 | 0 | 0 | 0 | JMdictDB - Site Access |
The gate page itself states the terms:
To use the JMdictDB Dictionary Database you need to have a password. Once the password has been entered, it will be set in your browser as a cookie, and will not need to be entered again until it changes.
Currently the password consists of four digits representing the current month.
So it is a cookie set by POST /jmwsgi/access.py, keyed on a value that rotates monthly.
Why the previously proposed fix does not apply
The original fix was to swap the e= selector for g= in three places in api/dict_query.py. That code is still unchanged (api/dict_query.py:66 is still row.find("input", {"name": "e"})), but changing it would fix nothing — both the e= and g= entry URLs now hit the same access wall, as the table above shows. The parser never gets far enough for the selector to matter.
More importantly, the gate is a deliberate anti-automation measure on a rotating credential. Programmatically defeating it is not an option worth pursuing: it would mean re-implementing a monthly-rotating login against a server that has explicitly asked us not to, and it would break again on the operator's next change. JMdictDB has to be abandoned as a runtime dependency.
Secondary defect (still valid, now the more damaging one)
The hardening note from the original report is now the main observable problem: a 200 with 0 parsed rows is indistinguishable from a genuine empty result, so a total upstream loss surfaces as a routine 404. This is why the outage was mis-diagnosed as a markup change in the first place.
Whatever the data-source decision, the parser should distinguish no results from parse/access failure and log the latter as an error.
Note also that no test covers DictQuery (tests/ has no dict_query module), so nothing in CI would have caught this.
Options
A. Bake JMdict locally (recommended)
Drop the scrape and ship the dictionary, matching what MarkAccent already does — the README describes the accent pipeline as "fully local and offline… no network calls", and this endpoint is the last thing contradicting that.
Measured 2026-09-14:
| Source | Compressed | Uncompressed | Has seq id? |
|---|---|---|---|
JMdict_e.gz (English-only XML) |
10.1 MB | 63 MB | yes — <ent_seq> |
JMdict.gz (all languages) |
21.2 MB | — | yes |
jmdict-eng (jmdict-simplified JSON) |
11.0 MB | — | yes |
Against the image's existing ~1.3 GB UniDic footprint this is negligible (<1%). <ent_seq> maps directly onto the existing WordResult.id, and <k_ele> / <r_ele> / <sense> map onto kanji / furigana / definitions, so the response schema is preservable.
Licence: JMdict is CC BY-SA 4.0 (EDRDG licence statement, §"Creative Commons Attribution-ShareAlike Licence (V4.0)"). Commercial use is permitted with attribution + ShareAlike — unlike tdmelodic, which was rejected in #62 as non-commercial. A NOTICE/attribution entry would be required (the repo currently has no LICENSE file at all).
B. Re-point at WWWJDIC
https://www.edrdg.org/cgi-bin/wwwjdic/wwwjdic is a different, ungated EDRDG endpoint — and api/sentence_query.py:55 already uses it, so it is a known-working path in this codebase. Verified returning clean EDICT-format data:
$ curl 'https://www.edrdg.org/cgi-bin/wwwjdic/wwwjdic?1ZUJ先生'
先生 [せんせい] /(n) (1) teacher/instructor/master/(n,n-suf) (2) (hon) sensei/...
先生方 [せんせいがた] /(n) (hon) (See 方・1) doctors/teachers/lawyers/
Easier to parse than the HTML it replaces. But it loses the id field — I scanned display codes 0–B and none emit JMdict sequence numbers (EntL…), so WordResult.id would have to be dropped or faked. It also keeps DictQuery network-dependent and scrape-shaped, i.e. the same fragility class that produced this issue and #57/#58.
C. Retire the endpoint
#62 records that DictQuery / SentenceQuery / UsageQuery "see little use" and explicitly parks their bugs as out of scope. If that still holds, removing the endpoint is cheaper than either fix above, and is the honest answer to shipping something that has been returning 404 for every input.
Decision needed
This is blocked on the #62 repo-split question rather than on implementation difficulty. A → B → C in descending order of effort; A is the only one consistent with the project's stated offline-first direction, and its cost is far lower than it looks (10 MB against a 1.3 GB image).
Reproduce
curl -s -X POST localhost:8000/api/DictQuery/ -H 'content-type: application/json' -d '{"word":"先生"}'
# -> 404 "No results found"
# the actual upstream condition:
curl -s 'https://www.edrdg.org/jmwsgi/srchres.py?s1=1&y1=1&t1=%E5%85%88%E7%94%9F&src=1&search=Search&svc=jmdict' | grep -o '<title>.*</title>'
# -> <title>JMdictDB - Site Access</title>Superseded diagnosis (original report, 2026-05-27)
Original text — kept for history. The e= → g= markup change described here was real at the time, but is no longer the operative failure.
EDRDG's backend (Stuart McGraw's JMdictDB) changed the markup of its search-results rows. Our parser is still hard-coded to the old format.
Fetching 先生 (in May 2026):
<tr class="resrow">rows are present (3 rows → upstream has data)- but the page contains 0 occurrences of
name="e"
| parser expects (old) | EDRDG actual (new) | |
|---|---|---|
| input name | name="e" |
name="g" (a checkbox) |
| value format | single entry id | seq.n~entrid (e.g. 2727400.1~2313503) |
| entry URL param | entr.py?...&e= |
entr.py?...&g= |
The proposed fix was to switch get_all_url (results-page parsing), the single-result redirect branch, and get_dict from the retired e= selector to the g= group selector.
Why this no longer applies: as of 2026-09-14 the whole /jmwsgi/ tree requires a monthly-rotating password, so neither selector is reachable.
Activity
[-]DictQuery returns 404 for valid words (先生): EDRDG/JMdictDB markup changed name="e" → name="g"[/-][+]DictQuery returns 404 for every word: EDRDG JMdictDB is now behind a site-wide password gate[/+]torridfish commentedon Sep 14, 2026
Re-diagnosed 2026-09-14; title and body rewritten.
tl;dr — the
e=→g=markup fix proposed in the original report would not have worked. The wholehttps://www.edrdg.org/jmwsgi/tree now sits behind a monthly-rotating password gate that returns HTTP 200, so the scraper never reaches a results page and our own404 No results foundfires atapi/dict_query.py:125-130. Both the olde=and newg=entry URLs hit the same wall — verified against the exact URL the code builds atapi/dict_query.py:44.The original diagnosis is preserved in a collapsed section at the bottom of the body.
Three options are written up with measured numbers: bake JMdict locally (
JMdict_e.gzis 10.1 MB against a 1.3 GB image, and<ent_seq>preservesWordResult.id), re-point at the ungated WWWJDIC endpoint thatapi/sentence_query.py:55already uses (losesid), or retire the endpoint per the #62 parking decision. No code changed — this needs the #62 repo-split call first.torridfish commentedon Sep 14, 2026
Closing per the decision to keep only
MarkAccent: rather than pick one of the three data-source options, the endpoint is being removed.PR #70 deletes
DictQuery,SentenceQueryandUsageQueryalong withapi/dependencies.pyand the sharedhttpx.AsyncClient, which makes the service request-time offline end to end. (The PR also carriesCloses #54, so this is just recording the decision now rather than leaving the issue open pending merge.)feat(api)!: remove DictQuery / SentenceQuery / UsageQuery