DEV Community

Cover image for The IETF datatracker never sets rfc_number on a draft: fixing my status check and running it on DKIM2
Jose Pollman
Jose Pollman

Posted on Fully Autonomous

The IETF datatracker never sets rfc_number on a draft: fixing my status check and running it on DKIM2

In an earlier post I wrote a short Python function that asks the IETF datatracker six questions about an Internet-Draft and counts the yes answers. I re-ran it three weeks later and two of its answers were wrong. One field does not mean what its name says. Another changes earlier than I said it does. This post fixes both, then runs the fixed version against every draft the DKIM working group has ever had.

Bug one: rfc_number is null on every draft

The old function counted a draft as published when rfc_number was not null. On 13 September it printed this for the DMARC working group's draft:

draft-ietf-dmarc-dmarcbis-41  (83 pages, expires 2025-10-06)
  ...
  no   published as an RFC
  5/6 signals present
Enter fullscreen mode Exit fullscreen mode

That draft had been published as RFC 9989 on 20 May 2026. The RFC Editor's record lists draft-ietf-dmarc-dmarcbis-41 as its source and RFC 7489 and RFC 9091 as the documents it obsoletes.

The datatracker stores an RFC as its own document record, named rfc9989. That record has rfc_number set. The draft that became it never does. You can see the scale of it with one request:

import json
import urllib.request

API = "https://datatracker.ietf.org/api/v1"

def get(path):
    sep = "&" if "?" in path else "?"
    with urllib.request.urlopen(f"{API}{path}{sep}format=json") as r:
        return json.load(r)

docs = get("/doc/document/?name__startswith=draft-ietf-dkim-&limit=100")["objects"]
print(len(docs), "drafts,", sum(d["rfc_number"] is not None for d in docs), "with rfc_number set")
Enter fullscreen mode Exit fullscreen mode
16 drafts, 0 with rfc_number set
Enter fullscreen mode Exit fullscreen mode

Nine of those sixteen are published RFCs. The link from a draft to its RFC lives in a separate table, under the relationship became_rfc:

rels = get("/doc/relateddocument/?source__name=draft-ietf-dmarc-dmarcbis&relationship__slug=became_rfc")
print(rels["objects"][0]["target"])
Enter fullscreen mode Exit fullscreen mode
/api/v1/doc/document/rfc9989/
Enter fullscreen mode Exit fullscreen mode

There is a cheaper tell on the draft itself. Its states list includes one state of the type draft, and that state's slug is active, expired or rfc. So the fixed check reads the state first and only makes the second request when it says rfc.

The earlier post also said that once rfc_number is set, "the draft is history". On a draft record it is never set, so that sentence described something that does not happen.

Bug two: stream is set at the call for adoption

The earlier post said that when stream stops being null, the document has been adopted. Its example of a draft with nothing going for it yet was draft-brotman-aggregate-performance-reporting. Here are its latest three events:

events = get("/doc/docevent/?doc__name=draft-brotman-aggregate-performance-reporting&limit=3")
for e in events["objects"]:
    print(e["time"][:10], e["desc"])
Enter fullscreen mode Exit fullscreen mode
2026-09-24 IETF WG state changed to <b>Call For Adoption By WG Issued</b>
2026-09-24 Changed group to <b>Mail Maintenance (MAILMAINT)</b>
2026-09-24 Changed stream to <b>IETF</b>
Enter fullscreen mode Exit fullscreen mode

On 24 September a call for adoption went out in the mailmaint working group, and the stream, the group and the working group state all changed in the same second. The name still starts with draft-brotman-. Nothing has been adopted. A call for adoption is a question to the group, and the answer can be no.

So a non-null stream means a working group is either considering the document or already owns it. The state of type draft-stream-ietf says which. These are the slugs worth knowing:

c-adopt   Call For Adoption By WG Issued
wg-doc    WG Document
sub-pub   Submitted to IESG for Publication
dead      Dead WG Document
Enter fullscreen mode Exit fullscreen mode

The fixed version drops the old "name contains ietf" signal, because the stream state answers the same question with more detail, and prints that state as its own column.

The fixed check

_states = {}

def states(doc):
    """Return {state type: slug}, e.g. {'draft': 'rfc', 'draft-stream-ietf': 'wg-doc'}."""
    out = {}
    for uri in doc["states"]:
        if uri not in _states:
            s = get(uri.removeprefix("/api/v1"))
            _states[uri] = (s["type"].rstrip("/").split("/")[-1], s["slug"])
        kind, slug = _states[uri]
        out[kind] = slug
    return out

def became_rfc(name):
    rels = get(f"/doc/relateddocument/?source__name={name}&relationship__slug=became_rfc")["objects"]
    return rels[0]["target"].rstrip("/").split("/")[-1] if rels else None

def row(doc):
    st = states(doc)
    rfc = became_rfc(doc["name"]) if st.get("draft") == "rfc" else None
    signals = [
        doc["stream"] is not None,
        doc["intended_std_level"] is not None,
        doc["ad"] is not None,
        doc["shepherd"] is not None,
        rfc is not None,
    ]
    return (f"{doc['name']:<38} {doc['rev']:>3}  {doc['time'][:10]}  "
            f"{st.get('draft', '-'):<8} {st.get('draft-stream-ietf', '-'):<8} "
            f"{sum(signals)}/5  {rfc or ''}")

def group_report(prefix):
    docs = get(f"/doc/document/?name__startswith={prefix}&limit=100")["objects"]
    for doc in sorted(docs, key=lambda d: d["time"], reverse=True):
        print(row(doc))

group_report("draft-ietf-dkim-")
Enter fullscreen mode Exit fullscreen mode

State lookups are cached because the same dozen state URIs repeat across every document.

Running it on a whole working group

name__startswith returns every draft under a prefix, so one request covers a working group's whole history. Output on 6 October 2026:

draft-ietf-dkim-dkim2-bcp               01  2026-09-09  active   wg-doc   1/5
draft-ietf-dkim-dkim2-spec              06  2026-08-28  active   wg-doc   1/5
draft-ietf-dkim-dkim2-dns               00  2026-07-20  active   wg-doc   1/5
draft-ietf-dkim-dkim2-header            00  2026-05-07  expired  dead     1/5
draft-ietf-dkim-dkim2-motivation        02  2026-05-06  expired  wg-doc   1/5
draft-ietf-dkim-replay-problem          00  2024-01-29  expired  -        0/5
draft-ietf-dkim-mailinglists            12  2020-01-21  rfc      wg-doc   5/5  rfc6377
draft-ietf-dkim-rfc4871bis              15  2020-01-21  rfc      sub-pub  5/5  rfc6376
draft-ietf-dkim-base                    10  2020-01-21  rfc      wg-doc   4/5  rfc4871
draft-ietf-dkim-rfc4871-errata          07  2015-10-14  rfc      wg-doc   4/5  rfc5672
draft-ietf-dkim-deployment              11  2015-10-14  rfc      -        4/5  rfc5863
draft-ietf-dkim-overview                12  2015-10-14  rfc      -        4/5  rfc5585
draft-ietf-dkim-ssp-requirements        05  2015-10-14  rfc      -        4/5  rfc5016
draft-ietf-dkim-threats                 03  2015-10-14  rfc      -        4/5  rfc4686
draft-ietf-dkim-ssp                     10  2015-10-14  rfc      -        4/5  rfc5617
draft-ietf-dkim-implementation-report   06  2011-03-28  expired  dead     2/5
Enter fullscreen mode Exit fullscreen mode

The columns are name, revision, the record's time, draft state, working group state, signals out of five (stream, intended status, AD, shepherd, RFC) and the RFC it became.

The prefix spans two eras. The bottom ten rows are the first DKIM working group, which produced RFC 4871 in May 2007 and its replacement, RFC 6376, in September 2011. The top five rows are DKIM2, and replay-problem from January 2024 sits between the two. All of them share one name prefix and one group record.

The time column is not a history. draft-ietf-dkim-base became RFC 4871 in 2007 and its record says 2020-01-21. Six records share 2015-10-14. Those are the dates the database rows were last touched. For when something happened, read the event log or the RFC.

Most of the old RFCs score 4/5 because their shepherd field is empty, while two from the same group have one. Treat a missing field on an old published record as missing data, not as a signal.

Every DKIM2 row scores 1/5: on the IETF stream, with no intended status, no AD and no shepherd yet. Two of the five have expired. The motivation document is still marked as a WG document, while the header document is marked dead.

The group record has its own history

The group's state changes and closing notes come from the same API:

for e in get("/group/groupevent/?group__acronym=dkim&type=changed_state&limit=20")["objects"]:
    print(e["time"][:10], e["desc"])
Enter fullscreen mode Exit fullscreen mode
2025-02-20 Charter approved, group active
2024-01-18 State changed to <b>Concluded</b> from Active
2023-02-21 Charter approved, group active
2011-09-26 Concluded group
2006-01-05 Started group
2005-10-01 Proposed group
Enter fullscreen mode Exit fullscreen mode

The closing note from January 2024, read with type=closing_note, says: "WG closing without coming to consensus on even its first deliverable. Little to no activity in several months." The current charter came a year later. Its milestones are in /group/groupmilestone/:

2025-04-30 not resolved | Adopt overview document
2025-04-30 not resolved | Adopt mechanism document
2025-11-30 not resolved | Adopt implementation guide
2025-12-31 not resolved | All documents submitted to the IESG
Enter fullscreen mode Exit fullscreen mode

The first two look met: adopted draft-ietf-dkim- drafts exist that fit both descriptions. Neither is marked resolved. Milestones are the weakest signal on the page. They slip routinely and the records often lag the work, so read them as the schedule the charter set, not as a status.

Following one reference

A third trap shows up when you follow a document's references through the API.

draft-ietf-dmarc-arc-to-historic-00, dated 22 April 2026, asks for RFC 8617 (ARC) to be reclassified as Historic and names DKIM2 as the direction the work has moved in. Its one reference to DKIM2 is informative, to draft-ietf-dkim-dkim2-motivation-02. In the table above, that document has expired.

Ask the API for the ARC draft's references and you get this:

for r in get("/doc/relateddocument/?source__name=draft-ietf-dmarc-arc-to-historic")["objects"]:
    print(r["relationship"].split("/")[-2], r["target"].split("/")[-2])
Enter fullscreen mode Exit fullscreen mode
refnorm rfc7208
refnorm rfc6376
refnorm rfc8174
refnorm rfc8617
refnorm rfc2119
refnorm rfc7489
replaces draft-adams-arc-experiment-conclusion
Enter fullscreen mode Exit fullscreen mode

Six normative references and a replaces link. Neither informative reference appears, so the DKIM2 pointer is invisible here. If you are tracing what a document depends on, read the references section of the document itself as well.

What this does not say

An expired motivation document does not mean DKIM2 has stalled. The spec draft reached revision 06 on 28 August and the BCP draft reached revision 01 on 9 September. Drafts lapse after six months unless revised, and a lapsed one stays in the record.

ARC is not Historic. The request is a revision 00 working group document, and its IESG state is "I-D Exists", which means the IESG has not started on it. That draft expires on 24 October 2026 unless a new revision is posted.

And citing the motivation draft was not a mistake. Revision 02 is dated 2 November 2025 and expired in early May 2026, so it was a live draft when the ARC document cited it in April.

The lesson I am keeping

The dmarcbis row was a known answer sitting in the output of the first post, and the function printed it as a no. A check that prints confident yes and no answers needs a case where you already know the answer, run every time the check changes. The fixed version now starts with two:

assert became_rfc("draft-ietf-dmarc-dmarcbis") == "rfc9989"
assert became_rfc("draft-ietf-dkim-base") == "rfc4871"
Enter fullscreen mode Exit fullscreen mode

The API itself is documented at datatracker.ietf.org/api, and none of it needs a key.

Written with AI assistance from my own notes and test results. I checked every fact and command before publishing.

Top comments (0)