XINCGer/Unity3DTraining · error · InvalidOperationException
SkipLastField cannot be called at the end of a stream
Error message
SkipLastField cannot be called at the end of a stream
What it means
SkipLastField() skips whatever field was last read via ReadTag(), but it requires a field tag to actually be pending. lastTag == 0 means ReadTag() already reached the logical end of stream, so there is nothing to skip and InvalidOperationException is thrown. It is a caller sequencing mistake, not corrupt data.
Solutions
- Restructure the parse loop: only call SkipLastField() inside the loop when the wire type is not handled, and never after ReadTag() returns 0.
- Guard the call: if (lastTag != 0) input.SkipLastField();
- Let generated message classes handle unknown fields; avoid manual tag loops where possible.
- Check whether code calls SkipLastField() unconditionally after loop exit — remove that call.
Example fix
// before
while ((tag = input.ReadTag()) != 0) { if (!HandleKnown(tag)) input.SkipLastField(); }
input.SkipLastField(); // stray call at end of stream
// after
while ((tag = input.ReadTag()) != 0)
{
if (!HandleKnown(tag)) input.SkipLastField();
} Defensive patterns
Strategy: try-catch
Validate before calling
if (tag == 0) return; // end of stream: nothing to skip if (lastTag == 0) return; input.SkipLastField();
Type guard
bool HasPendingField(CodedInputStream s, int lastTag) => lastTag != 0;
Try / catch
try { input.SkipLastField(); } catch (InvalidOperationException ex) { logger.LogWarning(ex, "SkipLastField at end of stream — parse loop bug"); } Prevention
- Only call SkipLastField() inside the ReadTag() loop for unhandled wire types.
- Never call it after ReadTag() returned 0 or twice in a row.
- Prefer generated message MergeFrom over hand-written tag loops.
When it happens
Trigger: Calling SkipLastField() after ReadTag() returned 0 (end of stream), or calling it twice in a row without an intervening ReadTag(), or calling it before any ReadTag() at all.
Common situations: Hand-rolled MergeFrom-style parsing loops where the 'while (tag != 0)' exit already ended the loop but a final SkipLastField() is still executed; unknown-field handling code copied from a generated parser without matching loop structure.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- SkipLastField called on an end-group tag, indicating that…
- SpaceLeft can only be called on CodedOutputStreams that are…
- Unexpected token type:
- EnumType is only valid for enum fields.
- MessageType is only valid for message fields.
AI-assisted analysis of XINCGer/Unity3DTraining@016f98412e (2026-09-12).
Data as JSON: /api/errors/d312f59cdd67b23d.
Report an issue: GitHub.
Appendix: source
Thrown at NetWorkAndResources/Socket_Protobuff/Assets/Plugins/Google.Protobuf/CodedInputStream.cs:400
/// <summary>
/// Skips the data for the field with the tag we've just read.
/// This should be called directly after <see cref="ReadTag"/>, when
/// the caller wishes to skip an unknown field.
/// </summary>
/// <remarks>
/// This method throws <see cref="InvalidProtocolBufferException"/> if the last-read tag was an end-group tag.
/// If a caller wishes to skip a group, they should skip the whole group, by calling this method after reading the
/// start-group tag. This behavior allows callers to call this method on any field they don't understand, correctly
/// resulting in an error if an end-group tag has not been paired with an earlier start-group tag.
/// </remarks>
/// <exception cref="InvalidProtocolBufferException">The last tag was an end-group tag</exception>
/// <exception cref="InvalidOperationException">The last read operation read to the end of the logical stream</exception>
public void SkipLastField()
{
if (lastTag == 0)
{
throw new InvalidOperationException("SkipLastField cannot be called at the end of a stream");
}
switch (WireFormat.GetTagWireType(lastTag))
{
case WireFormat.WireType.StartGroup:
SkipGroup(lastTag);
break;
case WireFormat.WireType.EndGroup:
throw new InvalidProtocolBufferException("SkipLastField called on an end-group tag, indicating that the corresponding start-group was missing");
case WireFormat.WireType.Fixed32:
ReadFixed32();
break;
case WireFormat.WireType.Fixed64:
ReadFixed64();
break;
case WireFormat.WireType.LengthDelimited:
var length = ReadLength();
SkipRawBytes(length);
break;View on GitHub (pinned to 016f98412e)