mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
a416bda5f3
Replace io.ReadAll(resp.Body) on the typed Call/callMultipart paths with a sync.Pool-backed bytes.Buffer + ReadFrom. Saves the 512B initial allocation that ReadAll grows from on every successful call. The pool only covers paths whose decoder copies strings out of the input (decodeResult delegates to goccy/go-json, which copies). CallRaw and callMultipartRaw return slices that alias the buffer storage, so they keep the io.ReadAll path; pooling there would need a defensive copy that defeats the saving. putRespBuf caps Cap() at 64 KiB before returning to the pool so a single oversized response (e.g. large getFile metadata) doesn't bloat the pool for the rest of the process. Bench delta on Call_BoolResponse: 14 allocs -> 13 allocs, 1842B -> 1331B, 526ns -> 479ns. Same shape on Call_StructResponse (16 -> 15, 1973B -> 1462B).
150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"github.com/goccy/go-json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
)
|
|
|
|
// multipartRequest is implemented by request structs that may carry an
|
|
// InputFile. The codegen emits this interface for any method whose IR
|
|
// MethodDecl.HasFiles is true.
|
|
//
|
|
// HasFile returns true if at least one file field is set; if false, the
|
|
// request is sent as plain JSON via the regular Call path.
|
|
//
|
|
// MultipartFiles returns one entry per file field that should be uploaded.
|
|
// The accompanying scalar/object fields are returned by MultipartFields.
|
|
type multipartRequest interface {
|
|
HasFile() bool
|
|
MultipartFiles() []MultipartFile
|
|
MultipartFields() map[string]string
|
|
}
|
|
|
|
// MultipartFile describes a single file part in a multipart upload.
|
|
type MultipartFile struct {
|
|
FieldName string
|
|
Filename string
|
|
Reader io.Reader
|
|
}
|
|
|
|
// callMultipart performs a multipart/form-data POST. It is invoked by Call
|
|
// when the request implements multipartRequest and HasFile() is true.
|
|
func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp multipartRequest) (Resp, error) {
|
|
var zero Resp
|
|
|
|
pr, pw := io.Pipe()
|
|
mw := multipart.NewWriter(pw)
|
|
|
|
// Stream-write the multipart body in a goroutine so we don't buffer
|
|
// large files in memory.
|
|
go func() {
|
|
defer func() { _ = pw.Close() }()
|
|
defer func() { _ = mw.Close() }()
|
|
for k, v := range mp.MultipartFields() {
|
|
if err := mw.WriteField(k, v); err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
for _, f := range mp.MultipartFiles() {
|
|
part, err := mw.CreateFormFile(f.FieldName, f.Filename)
|
|
if err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
if _, err := io.Copy(part, f.Reader); err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
url := b.base + "/bot" + b.token + "/" + method
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, pr)
|
|
if err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
return zero, &NetworkError{Err: err}
|
|
}
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
req.Header["Accept"] = headerJSONValue
|
|
|
|
resp, err := b.http.Do(req)
|
|
if err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return zero, ctxErr
|
|
}
|
|
return zero, &NetworkError{Err: err}
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
buf := respBufPool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
defer putRespBuf(buf)
|
|
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
return zero, &NetworkError{Err: err}
|
|
}
|
|
return decodeResult[Resp](b.codec, buf.Bytes())
|
|
}
|
|
|
|
// callMultipartRaw is callMultipart's sibling that returns the raw result
|
|
// JSON instead of decoding into a typed value. Used by generated method
|
|
// wrappers whose return type is a sealed-interface union.
|
|
func callMultipartRaw(ctx context.Context, b *Bot, method string, mp multipartRequest) (json.RawMessage, error) {
|
|
pr, pw := io.Pipe()
|
|
mw := multipart.NewWriter(pw)
|
|
|
|
go func() {
|
|
defer func() { _ = pw.Close() }()
|
|
defer func() { _ = mw.Close() }()
|
|
for k, v := range mp.MultipartFields() {
|
|
if err := mw.WriteField(k, v); err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
for _, f := range mp.MultipartFiles() {
|
|
part, err := mw.CreateFormFile(f.FieldName, f.Filename)
|
|
if err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
if _, err := io.Copy(part, f.Reader); err != nil {
|
|
_ = pw.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
url := b.base + "/bot" + b.token + "/" + method
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, pr)
|
|
if err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
return nil, &NetworkError{Err: err}
|
|
}
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
req.Header["Accept"] = headerJSONValue
|
|
|
|
resp, err := b.http.Do(req)
|
|
if err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return nil, ctxErr
|
|
}
|
|
return nil, &NetworkError{Err: err}
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
_ = pr.CloseWithError(err)
|
|
return nil, &NetworkError{Err: err}
|
|
}
|
|
return decodeResultRaw(b.codec, raw)
|
|
}
|