Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Assert every refusal the `uri` format checker makes, including a truncated percent escape

Open
#173 3 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@soyeladice-svg is already working on this.

Since Sep 29, 2026.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
57/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
backend, testing

Research direction

Start in internal/validation/validator_test.go at TestURIFormatUsesStrictRFC3986ASCII, then read validateRFC3986URI in internal/validation/validator.go:94 and derive a test row for each refusal. Add direct exact-message assertions alongside the existing pack-level /id diagnostic checks, including the specified accepted query escape. Done means the table catches each requested mutation and the listed Go checks pass; the pull request must also report mutation outcomes and include the required declaration and DCO sign-off.

Written by the indexing model from the issue text.

Description

go good first issue

Goal

Hold each refusal in validateRFC3986URI (internal/validation/validator.go:94) by the message that names its rule. That function is the checker registered for JSON Schema's format: "uri", and every pack's id passes through it. Several of its refusals are held by nothing today. Two of them, the length check in front of an escape's hex digits and the check on url.Parse's error, are all that stands between a malformed pack id and a panic.

Why

func validateRFC3986URI(value any) error {
	text, ok := value.(string)
	if !ok {
		return nil
	}
	if text == "" {
		return errors.New("URI is empty")
	}
	for index := 0; index < len(text); index++ {
		character := text[index]
		if character == '%' {
			if index+2 >= len(text) || !isHex(text[index+1]) || !isHex(text[index+2]) {
				return errors.New("URI contains an invalid percent escape")
			}
			index += 2
			continue
		}
		if !isRFC3986Character(character) {
			return errors.New("URI contains a character outside RFC 3986")
		}
	}
	if strings.Count(text, "#") > 1 {
		return errors.New("URI contains more than one fragment delimiter")
	}
	parsed, err := url.Parse(text)
	if err != nil {
		return errors.New("URI cannot be parsed")
	}
	if parsed.Scheme == "" || !uriSchemePattern.MatchString(parsed.Scheme) {
		return errors.New("URI is not absolute")
	}
	return nil
}

CompileSchema registers it as the uri format (validator.go:73), and both bundled pack schemas give the pack's id that format (internal/artifacts/jps/*/schema.json). So every surface that validates a pack runs this function over an author's id: spec validate, packs validate, the MCP validate tool, and the evaluator's own admission of a pack.

Measured on current main (f98d4c9), each relaxation alone passes the whole gate: go vet ./... is clean, go test ./... passes, and go run ./cmd/jpack spec test-conformance --quiet passes.

relaxation (validator.go) gate what a pack id then does
line 105: the whole percent-escape condition → if false { green https://example.com/?q=%, ?q=%Z0 and ?q=%0Z all validate as conforming ids
line 105: delete index+2 >= len(text) || green https://example.com/% panics: index out of range [21] with length 21
line 105: index+2 >= len(text) → index+2 > len(text) green https://example.com/%4 panics the same way
line 105: delete || !isHex(text[index+1]) green https://example.com/?q=%Z0 validates as a conforming id
line 105: delete || !isHex(text[index+2]) green https://example.com/?q=%0Z validates as a conforming id
line 115: if strings.Count(text, "#") > 1 { → if false { green https://example.com/#a#b validates as a conforming id
line 119: if err != nil { → if err != nil && false { green https://[::1/ panics: nil pointer dereference on parsed.Scheme
line 99: if text == "" { → if false { green "" is still refused, but as URI is not absolute; only an assertion on the message can see this one

The panics are not contained. With the first relaxation, jpack spec validate on a pack whose id is https://example.com/% exits 2 with a Go stack trace. The shipping build exits 1 and reports JPS-STRUCTURE-FORMAT-URI at /id. Over MCP, the same document sent to the validate tool takes the whole server process down, so a ping sent after it on the same connection is never answered. Nothing in the runtime calls recover().

Why the query rows matter. url.Parse checks percent escapes in the path and the fragment, but not in the query. Measured against the standard library:

https://example.com/%Z0       url.Parse err=invalid URL escape "%Z0"
https://example.com/?q=%Z0    url.Parse err=<nil>
https://example.com/?q=%0Z    url.Parse err=<nil>

So in the path, a relaxed hex check is still refused one step later, as URI cannot be parsed. The pack still gets JPS-STRUCTURE-FORMAT-URI at /id, and a row that asserts only that code cannot tell which layer refused. In the query, nothing else is looking: the loop above is the whole rule.

What holds it today. TestURIFormatUsesStrictRFC3986ASCII (internal/validation/validator_test.go:208) is the only test of this checker. It has one percent row, https://example.com/%ZZ. Both digits there are bad, so deleting either isHex clause leaves the other to refuse it; the escape sits in the path, where url.Parse refuses it anyway; and the escape is not truncated, so neither length relaxation is reached. The test asserts the diagnostic code at /id and never the checker's message. That is why even deleting the whole percent-escape condition leaves the suite green. No test calls validateRFC3986URI directly, and none of its six messages appears in any test file:

$ grep -rn --include='*_test.go' -e 'invalid percent escape' -e 'fragment delimiter' -e 'cannot be parsed' -e 'URI is empty' -e 'outside RFC 3986' -e 'URI is not absolute' .
$ echo $?
1

Scope

  • Change internal/validation/validator_test.go only.
  • Derive the rows from the source, not from the table above. Walk validateRFC3986URI and list every return errors.New(…). For each one, in the pull request, name the row that holds it. The percent-escape condition has three clauses, and its length clause can be broken two ways (deleted, or loosened by one), so it takes four rows: a % with nothing after it, a % with one digit and then the end of the text, a bad first digit, and a bad second digit.
  • Each row carries exactly one obstacle, and is otherwise a URI the checker accepts. Put the bad-digit escapes in the query, where url.Parse does not check them. In the path, a row passes on broken code because the next layer refuses it for a different reason.
  • For each row, assert two things:
    • Call validateRFC3986URI directly and assert the exact message that names the rule. This is the assertion that does the work: at the pack level, every refusal reads Value does not satisfy the required uri format.
    • As the existing test does, set the value as the pack's id and assert JPS-STRUCTURE-FORMAT-URI at /id. That keeps the checker wired to the schema.
  • Include accepted inputs in the same table, at least one well-formed escape in the query with lower-case hex digits beside the existing urn and path-escape rows. Without them, a checker that refused everything would satisfy the table.
  • Derive every expected message by running the unmutated code.
  • A row that panics fails its test as it should; do not add a recover. Say in the pull request which mutation checks below fail by panic and which fail by assertion.
  • Do not change validator.go, the bundled schemas, or any diagnostic text. If a message reads wrong to you, say so in the pull request.

Acceptance criteria

  • One table in internal/validation/validator_test.go, which may be TestURIFormatUsesStrictRFC3986ASCII extended, with one obstacle per row. Each refused row asserts validateRFC3986URI's exact message and JPS-STRUCTURE-FORMAT-URI at /id.

  • Accepted inputs in the same table, including a query escape with lower-case hex digits.

  • The pull request lists every return errors.New(…) in validateRFC3986URI next to the row that holds it.

  • Mutation check, one at a time, on internal/validation/validator.go. Each is reported in the pull request with the row that caught it and whether it failed by assertion or by panic:

    • (1) delete index+2 >= len(text) || from line 105;
    • (2) change it to index+2 > len(text);
    • (3) delete || !isHex(text[index+1]);
    • (4) delete || !isHex(text[index+2]);
    • (5) replace if strings.Count(text, "#") > 1 { with if false {;
    • (6) at line 119, replace if err != nil { with if err != nil && false {.

    After each, restore with git checkout -- internal/validation/validator.go and confirm that git status --porcelain prints nothing.

  • env GO111MODULE=on go fmt ./... leaves git status clean.

  • env GO111MODULE=on go vet ./... is clean.

  • env GO111MODULE=on go test ./... passes.

  • env GO111MODULE=on go run ./cmd/jpack spec test-conformance --quiet passes.

  • env GO111MODULE=on CGO_ENABLED=0 go build -trimpath ./cmd/jpack succeeds; delete the resulting ./jpack binary and confirm git status is clean.

  • The pull request carries its one-line Material-decision impact: declaration (tests only, no material-decision change).

  • Every commit includes a DCO sign-off created with git commit -s.

Contributor learning

A bounds check in front of an index is a refusal like any other, and when it goes nothing refuses anything: the program panics. The test that holds it is the one whose input ends exactly where the index would run past. The other lesson is layers. The standard library's url.Parse also rejects bad escapes, but only in some parts of a URI. A test that puts its bad input where a second layer happens to catch it passes whether or not the first layer works, so a row has to be placed where its own rule is the only one looking, and it has to assert the message that names that rule rather than a code the whole checker shares.

Dominant language
Go
Stars
2
Forks
6
Avg merge
3h 32m
Merged PRs (30d)
73

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Judgment-Pack/judgment-pack-runtime

All issues in Judgment-Pack/judgment-pack-runtime

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.