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

  1. Restructure the parse loop: only call SkipLastField() inside the loop when the wire type is not handled, and never after ReadTag() returns 0.
  2. Guard the call: if (lastTag != 0) input.SkipLastField();
  3. Let generated message classes handle unknown fields; avoid manual tag loops where possible.
  4. 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

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


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)