Skip to content

Local telegram-bot-api compatibility issue with chunked request bodies generated by io.Pipe() #285

Description

@jeng8

Summary

When using github.com/go-telegram/bot v1.21.0 with a local Telegram Bot API server (telegram-bot-api --local), requests may fail with:

error decode response body for method getMe:
unexpected end of JSON input

After investigation, the issue appears to be related to chunked transfer encoding.

rawRequest() streams request bodies via io.Pipe(), causing Go to send:

Transfer-Encoding: chunked

because the request body length is unknown.

Some local telegram-bot-api --local servers appear to reject chunked request bodies and respond with:

HTTP 400 Bad Request
Content-Length: 0

which eventually leads to the JSON decoding failure.


Environment

  • Go: go1.25.0 darwin/arm64
  • github.com/go-telegram/bot: v1.21.0
  • telegram-bot-api: local mode (--local)
  • OS: macOS (arm64)

Steps to reproduce

1. Start local Bot API server

telegram-bot-api \
  --api-id=xxx \
  --api-hash=xxx \
  --local \
  --http-port=7070

2. Run code

package main

import (
	"log"

	"github.com/go-telegram/bot"
)

func main() {
	b, err := bot.New(
		"YOUR_BOT_TOKEN",
		bot.WithServerURL("http://127.0.0.1:7070"),
	)
	if err != nil {
		log.Fatal(err)
	}

	_ = b
}

Observed behavior

Request:

POST /bot<TOKEN>/getMe
Transfer-Encoding: chunked
Content-Type: multipart/form-data; boundary=...

Response:

HTTP 400 Bad Request
Content-Length: 0

Client error:

error call getMe, error decode response body for method getMe, unexpected end of JSON input

Root cause

rawRequest() creates a streaming request body:

pr, pw := io.Pipe()
form := multipart.NewWriter(pw)

Because the body length is unknown, Go automatically uses:

Transfer-Encoding: chunked

instead of:

Content-Length: xxx

The local Bot API server appears to reject chunked request bodies and returns an empty 400 response.

Therefore, the issue seems to be related to chunked transfer encoding rather than multipart itself.


Additional verification

Sending a request with a known Content-Length works correctly.

For example:

curl -X POST \
  -H "Content-Type: application/json" \
  -d 'null' \
  http://127.0.0.1:7070/bot<TOKEN>/getMe

returns:

200 OK

Also, buffering the request body and explicitly setting:

req.ContentLength = int64(len(buf))
req.TransferEncoding = nil

eliminates the problem.


Workaround

A custom RoundTripper that converts unknown-length request bodies into fixed-length bodies resolves the issue:

type contentLengthTransport struct {
	base http.RoundTripper
}

func (t *contentLengthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil && req.ContentLength <= 0 {
		buf, err := io.ReadAll(req.Body)
		_ = req.Body.Close()
		if err != nil {
			return nil, err
		}

		if len(buf) == 0 {
			req.Body = http.NoBody
			req.GetBody = func() (io.ReadCloser, error) {
				return http.NoBody, nil
			}
		} else {
			req.Body = io.NopCloser(bytes.NewReader(buf))
			req.GetBody = func() (io.ReadCloser, error) {
				return io.NopCloser(bytes.NewReader(buf)), nil
			}
		}

		req.ContentLength = int64(len(buf))
		req.TransferEncoding = nil
	}

	return t.base.RoundTrip(req)
}

Usage:

transport := http.DefaultTransport

client := &http.Client{
	Transport: &contentLengthTransport{
		base: transport,
	},
	Timeout: 10 * time.Second,
}

b, err := bot.New(
	token,
	bot.WithHTTPClient(10*time.Second, client),
	bot.WithServerURL("http://127.0.0.1:7070"),
)
if err != nil {
	log.Fatal(err)
}

With this custom transport, bot.New() and subsequent API calls work correctly against the local Bot API server.


Possible improvement

Instead of streaming multipart requests through io.Pipe(), buffering the body and sending it with a known Content-Length may improve compatibility with local Bot API servers.

For example:

var body bytes.Buffer

form := multipart.NewWriter(&body)

// write fields ...

form.Close()

req, err := http.NewRequestWithContext(
	ctx,
	http.MethodPost,
	u,
	&body,
)

req.ContentLength = int64(body.Len())

which results in:

Content-Length: xxx

instead of:

Transfer-Encoding: chunked
```.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions