Skip to content

📝 [Proposal]: Provide an effective TCP disconnect handle #4145

Description

@bincooo

Feature Proposal Description

First of all, I would like to thank the developers for their efforts.
In order to better support stream/sse processing, could you add a listening implementation for TCP disconnection? I have encountered many such situations where the front end actively disconnects the request, but the backend fails to detect it in a timely manner. It is only when the backend task is completed or an attempt is made to write to the stream that a read/write exception is issued.

Alignment with Express API

I have made a crude implementation, and I am not sure if there is a related implementation for this project. I would like to offer a suggestion:

package v1

import (
	"context"
	"errors"
	"net"
	"syscall"
	"time"

	"github.com/gofiber/fiber/v3"
)

type abortCtx struct {
	parent context.Context
	done   chan struct{}
	err    error
}

// 创建一个监听客户端断开的 Context
func newAbort(ctx fiber.Ctx) (context.Context, context.CancelFunc) {
	cctx := &abortCtx{
		parent: ctx.Context(),
		done:   make(chan struct{}),
	}

	loop := true
	cancel := func() {
		loop = false
		select {
		case <-cctx.done:
			// 已经关闭
		default:
			cctx.err = context.Canceled
			close(cctx.done)
		}
	}

	ch := make(chan byte, 1)
	conn := ctx.RequestCtx().Conn()
	go func() {
		for loop {
			if isClosed(conn) {
				ch <- 1
				loop = false
			}
			time.Sleep(time.Millisecond * 500)
		}
	}()

	// 后台轮询 IsAbandoned
	go func() {
		select {
		case <-cctx.parent.Done():
			select {
			case <-cctx.done:
			default:
				cctx.err = cctx.parent.Err()
				close(cctx.done)
			}
			return

		case <-ch:
			select {
			case <-cctx.done:
			default:
				cctx.err = context.Canceled
				close(cctx.done)
			}
			return

		case <-cctx.done:
			return
		}
	}()

	return cctx, cancel
}

func (c *abortCtx) Deadline() (time.Time, bool) {
	return c.parent.Deadline()
}

func (c *abortCtx) Done() <-chan struct{} {
	return c.done
}

func (c *abortCtx) Err() error {
	select {
	case <-c.done:
		return c.err
	default:
		return nil
	}
}

func (c *abortCtx) Value(key any) any {
	return c.parent.Value(key)
}

func isClosed(conn net.Conn) bool {
	// 获取底层文件描述符
	rawConn, ok := conn.(syscall.Conn)
	if !ok {
		return false
	}

	var closed bool
	rc, err := rawConn.SyscallConn()
	if err != nil {
		return false
	}

	_ = rc.Read(func(fd uintptr) bool {
		buf := make([]byte, 1)

		// 使用 MSG_PEEK:查看数据但不消费
		n, _, ioerr := syscall.Recvfrom(int(fd), buf, syscall.MSG_PEEK|syscall.MSG_DONTWAIT)
		if n == 0 && ioerr == nil {
			// EOF:对端关闭
			closed = true
		}

		if errors.Is(ioerr, syscall.ECONNRESET) {
			closed = true
		}

		// EAGAIN/EWOULDBLOCK = 没有数据但连接正常
		return true
	})

	return closed
}

HTTP RFC Standards Compliance

API Stability

Feature Examples

-

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have searched for existing issues that describe my proposal before opening this one.
  • I understand that a proposal that does not meet these guidelines may be closed without explanation.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions