{"id":"9921cdd483d94cee","repo":"jackc/pgx","slug":"invalid-read-of-large-object","errorCode":null,"errorMessage":"invalid read of large object","messagePattern":"invalid read of large object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"large_objects.go","lineNumber":132,"sourceCode":"\t\tif expected == 0 {\n\t\t\tbreak\n\t\t} else if expected > maxLargeObjectMessageLength {\n\t\t\texpected = maxLargeObjectMessageLength\n\t\t}\n\n\t\tres := pgtype.PreallocBytes(p[nTotal:])\n\t\terr := o.tx.QueryRow(o.ctx, \"select loread($1, $2)\", o.fd, expected).Scan(&res)\n\t\t// We compute expected so that it always fits into p, so it should never happen\n\t\t// that PreallocBytes's ScanBytes had to allocate a new slice.\n\t\tnTotal += len(res)\n\t\tif err != nil {\n\t\t\treturn nTotal, err\n\t\t}\n\n\t\tif len(res) < expected {\n\t\t\treturn nTotal, io.EOF\n\t\t} else if len(res) > expected {\n\t\t\treturn nTotal, errors.New(\"invalid read of large object\")\n\t\t}\n\t}\n\n\treturn nTotal, nil\n}\n\n// Seek moves the current location pointer to the new location specified by offset.\nfunc (o *LargeObject) Seek(offset int64, whence int) (n int64, err error) {\n\terr = o.tx.QueryRow(o.ctx, \"select lo_lseek64($1, $2, $3)\", o.fd, offset, whence).Scan(&n)\n\treturn n, err\n}\n\n// Tell returns the current read or write location of the large object descriptor.\nfunc (o *LargeObject) Tell() (n int64, err error) {\n\terr = o.tx.QueryRow(o.ctx, \"select lo_tell64($1)\", o.fd).Scan(&n)\n\treturn n, err\n}\n","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/jackc/pgx/blob/ec1a0befd22592cffffdeeb0a50311b506372f4c/large_objects.go#L114-L150","documentation":"Returned by LargeObject.Read when PostgreSQL's loread() built-in returns MORE bytes than the requested length. The read loop computes an 'expected' byte count that always fits into the caller's buffer, so the server returning extra bytes is a protocol violation. Per the code comment, PreallocBytes.Scan should never need to allocate a new slice either. This is essentially a sanity guard against a misbehaving server or corrupted large-object stream.","triggerScenarios":"Calling (*LargeObject).Read(p []byte) (large_objects.go:110) where the underlying 'select loread($1, $2)' query returns a byte slice longer than the 'expected' argument passed. Only reachable when the server violates the loread contract.","commonSituations":"Connecting to a buggy/proxied PostgreSQL-compatible server (e.g. a sharded middleware or a fork) that mishandles loread length; a corrupted pg_largeobject table; extremely rarely against vanilla PostgreSQL. Also seen with concurrent Truncate/Write racing a Read.","solutions":["Verify you are reading a valid, open large-object descriptor obtained from lo_open within the same transaction.","Ensure no concurrent Truncate/Write is mutating the large object during the Read.","Test against a stock PostgreSQL to rule out server/proxy misbehavior; if a proxy/middleware is in front, check its loread forwarding logic.","If using a non-vanilla server, report the protocol violation upstream."],"exampleFix":"// before: read into a buffer while another goroutine truncates\nlo.Read(buf)\n\n// after: perform reads and truncation serially in one transaction\nn, err := lo.Read(buf)\nif err != nil {\n    return fmt.Errorf(\"lo read failed: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// LargeObject.Read already returns the error via io.Reader interface.\nn, err := lo.Read(buf)\nif err != nil {\n    if errors.Is(err, io.EOF) {\n        // normal end of large object\n        return n, nil\n    }\n    return n, fmt.Errorf(\"large object read failed (possible server protocol violation): %w\", err)\n}","preventionTips":["Keep reads, writes, and truncation on a given large object within a single serial transaction.","Validate the fd came from lo_open in the same Tx before reading.","Against non-vanilla PostgreSQL-compatible servers, regression-test large-object round trips."],"tags":["large-objects","io","protocol","postgresql"],"analyzedSha":"ec1a0befd22592cffffdeeb0a50311b506372f4c","analyzedAt":"2026-08-04T22:52:11.263Z","schemaVersion":2}