Skip to content

POST locks/batch returns 500 instead of 400 when ref is not an object (e.g. "ref": "refs/heads/main") #77

Description

@matt-edmondson

What's wrong

LockFanOutRequest.TryParse reads the ref with a chained indexer:

// GitLfsCache/Locks/LockFanOutRequest.cs:62
JsonValues.String(root["ref"]?["name"]),

JsonNode's string indexer calls AsObject(). When root["ref"] is present but is not a JsonObject (a string, number, bool or array), it throws InvalidOperationException: The node must be of type 'JsonObject'. before JsonValues.String runs. LockRouteHandler.FanOutAsync only catches System.Text.Json.JsonException around the parse (GitLfsCache/Endpoints/LockRouteHandler.cs:106-115), and TryParse is called outside that try (LockRouteHandler.cs:117). Nothing else in the pipeline handles the exception (GitLfsCacheHandler, MapGitLfsCache, and the tool host have no exception middleware), so the client gets an unhandled-exception 500.

This goes against the locks spec's As built note (locks-subsystem-design.md L270):`` "All untrusted JSON is read through a JsonValues helper ... reading a client's or an upstream's body directly turns a malformed request into an unhandled exception and a 500." This is the one read in the fan-out parser that still does that. The type docs of `LockFanOutRequest.TryParse` also say "Every malformed shape is refused rather than partly honoured".

Failure scenario

POST /github/studio/game.git/info/lfs/locks/batch
{"operation":"lock","paths":["Content/A.uasset"],"ref":"refs/heads/main"}

A client that flattens ref to a string (the listing endpoint takes it as a refspec query string, so this mistake is easy to make when writing the vendored-client patch the spec describes) gets 500 with no body, where it should get 400. "ref": ["x"] and "ref": 1 fail the same way.

Reproduced with a standalone System.Text.Json (net10.0) program: JsonNode.Parse("{\"ref\":\"refs/heads/main\"}")["ref"]?["name"] throws System.InvalidOperationException: The node must be of type 'JsonObject'.

Suggested fix

  • Read the ref with a type check first, e.g. root["ref"] is JsonObject refObject ? JsonValues.String(refObject["name"]) : null, or add a JsonValues.Object(...) helper.
  • Decide whether a ref that is present but not an object should be refused with 400 or treated as absent. Refusing matches the parser's stated "every malformed shape is refused" rule, and means a lock is never taken without the ref the client meant to send.

Acceptance criteria

  • LockFanOutRequest.TryParse returns false and does not throw for ref given as a string, number, bool or array. There are currently no unit tests for LockFanOutRequest in GitLfsCache.Tests/.
  • An integration test in LockFanOutTests shows POST locks/batch with "ref": "refs/heads/main" returns 400 and makes no upstream call.
  • {"ref": {"name": "refs/heads/main"}} behaves as it does today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions