ory/hydra · error

unsupported operation: %s

Error message

unsupported operation: %s

What it means

ApplyJSONPatch in oryx/jsonx applies a JSON Patch but only allows the operations add, remove, and replace (see opAllowList). Other RFC 6902 operations — copy, move, and test — are rejected because the underlying evanphx/json-patch implementation has known bugs with them (see github.com/evanphx/json-patch/pull/158). The error names the offending operation kind.

Source

Thrown at oryx/jsonx/patch.go:65

// is invalid or if the patch includes paths that are denied. denyPaths is a
// list of path globs (interpreted with [glob.Compile] that are not allowed to
// be patched.
func ApplyJSONPatch[T any](p json.RawMessage, object T, denyPaths ...string) (result T, err error) {
	patch, err := jsonpatch.DecodePatch(p)
	if err != nil {
		return result, errors.WithStack(err)
	}

	denyPattern := fmt.Sprintf("{%s}", strings.ToLower(strings.Join(denyPaths, ",")))
	matcher, err := glob.Compile(denyPattern, '/')
	if err != nil {
		return result, errors.WithStack(err)
	}

	for _, op := range patch {
		// Some operations are buggy, see https://github.com/evanphx/json-patch/pull/158
		if isUnsupported(op) {
			return result, errors.Errorf("unsupported operation: %s", op.Kind())
		}
		path, err := op.Path()
		if err != nil {
			return result, errors.Errorf("error parsing patch operations: %v", err)
		}
		if matcher.Match(strings.ToLower(path)) {
			return result, errors.Errorf("patch includes denied path: %s", path)
		}

		// JSON patch officially rejects replacing paths that don't exist, but we want to be more tolerant.
		// Therefore, we will ensure that all paths that we want to replace exist in the original document.
		if op.Kind() == "replace" && !isElementAccess(path) {
			op["op"] = new(json.RawMessage(`"add"`))
		}
	}

	original, err := json.Marshal(object)
	if err != nil {

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Rewrite the patch using only add, remove, and replace: express move as remove+add, copy as read+add, and drop test preconditions.
  2. Split the patch: perform test/copy/move client-side and send only the resulting add/remove/replace operations.
  3. If you need copy/move server-side, apply the patch with jsonpatch directly (bypassing ApplyJSONPatch) after validating paths yourself.

Example fix

// before
[{"op": "move", "from": "/a", "path": "/b"}]
// after
[{"op": "add", "path": "/b", "value": <value of /a>}, {"op": "remove", "path": "/a"}]
Defensive patterns

Strategy: validation

Validate before calling

allowed := map[string]bool{"add": true, "remove": true, "replace": true}
for _, op := range patchOps {
    if !allowed[op["op"].(string)] {
        return fmt.Errorf("operation %v not allowed; use add/remove/replace", op["op"])
    }
}

Prevention

When it happens

Trigger: Calling ApplyJSONPatch with a patch document containing any operation whose "op" is "copy", "move", or "test" (anything not in {add, remove, replace}); e.g., via patchOAuth2Client receiving a user-supplied PATCH body.

Common situations: API clients crafting RFC 6902 patches that include 'test' preconditions or 'move'/'copy' for convenience; generic JSON Patch tooling that produces all six standard operations.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/47d293d44ebe295a. Report an issue: GitHub.