dotnet/aspnetcore · error · RuntimeException

Error reading JSON.

Error message

Error reading JSON.

What it means

Thrown by GsonHubProtocol.parseMessages when the underlying JsonReader raises an IOException while parsing the payload. It wraps the original IOException as the cause of a RuntimeException. It signals genuinely malformed JSON inside an otherwise correctly framed message (framing passed the 0x1E check at line ~60; this fires later during per-message parse).

Solutions

  1. Inspect the cause IOException for the Gson position - it usually pinpoints the bad token.
  2. Confirm both ends use JsonHubProtocol (server-side AddJsonProtocol / the default) and not MessagePack.
  3. Log the raw frame that failed; compare it against what the server serialized.
  4. Update client and server to matching SignalR versions.

Example fix

// before: mismatched protocols
// server: builder.AddMessagePackProtocol();
// client: new HubConnectionBuilder().withHubProtocol(new JsonHubProtocol())...

// after: both use JSON
// server: defaults to JSON (no AddMessagePackProtocol)
// client: new HubConnectionBuilder().withHubProtocol(new JsonHubProtocol())...
Defensive patterns

Strategy: try-catch

Validate before calling

// No caller-side validation prevents a malformed server payload.
// Validate protocol agreement before start: ensure server uses JSON, not MessagePack.
boolean serverUsesJsonProtocol = true; // from server config / deployment manifest

Type guard

// Cannot type-guard remote JSON. Instead, log the raw frame and the Gson token position
// from the wrapped IOException cause to pinpoint the corruption.

Try / catch

try {
  protocol.parseMessages(payload, null);
} catch (RuntimeException e) {
  if (e.getCause() instanceof IOException && "Error reading JSON.".equals(e.getMessage())) {
    // log payload + ((IOException) e.getCause()).getMessage(); reconcile protocol/version
  } else throw e;
}

Prevention

When it happens

Trigger: A frame is well-delimited (ends with 0x1E) but its JSON is invalid - unterminated object, bad escape, stray token, truncated inner payload. The for-loop over split messages hits an unparseable token and Gson throws IOException.

Common situations: A proxy/load balancer corrupts the body; the server and client disagree on the hub protocol (e.g. server uses MessagePack but client configured JsonHubProtocol); a buffer overrun sliced the middle of a JSON object while leaving the trailing 0x1E; version skew where the server emits a field shape the client's Gson cannot parse.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/be0185afdba64d12. Report an issue: GitHub.

Appendix: source

Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/GsonHubProtocol.java:220

                    case STREAM_INVOCATION:
                    case CANCEL_INVOCATION:
                        throw new UnsupportedOperationException(String.format("The message type %s is not supported yet.", messageType));
                    case PING:
                        hubMessages.add(PingMessage.getInstance());
                        break;
                    case CLOSE:
                        if (error != null) {
                            hubMessages.add(new CloseMessage(error));
                        } else {
                            hubMessages.add(new CloseMessage());
                        }
                        break;
                    default:
                        break;
                }
            }
        } catch (IOException ex) {
            throw new RuntimeException("Error reading JSON.", ex);
        }

        return hubMessages;
    }

    @Override
    public ByteBuffer writeMessage(HubMessage hubMessage) {
        return ByteBuffer.wrap((gson.toJson(hubMessage) + RECORD_SEPARATOR).getBytes(StandardCharsets.UTF_8));
    }

    private ArrayList<Object> bindArguments(JsonArray argumentsToken, List<Type> paramTypes) {
        if (argumentsToken.size() != paramTypes.size()) {
            throw new RuntimeException(String.format("Invocation provides %d argument(s) but target expects %d.", argumentsToken.size(), paramTypes.size()));
        }

        ArrayList<Object> arguments = null;
        if (paramTypes.size() >= 1) {
            arguments = new ArrayList<>();

View on GitHub (pinned to 3600ca084e)