req

package module
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: MIT Imports: 18 Imported by: 8

README

go-req

GoDoc Build Status Coverage Status Go Report Card

Declarative golang HTTP client

package req_test

import (
    "fmt"
    "github.com/wenerme/go-req"
    "net/http"
)

func ExampleRequest() {
    // reusable client
    client := req.Request{
        BaseURL: "https://httpbin.org",
        Options: []interface{}{req.JSONEncode, req.JSONDecode},
    }
  
    // dump request and response with body
    client = client.WithHook(req.DebugHook(&req.DebugOptions{Body: true}))
    
    // send request with declarative override
    var out PostResponse
    var r *http.Response
    err := client.With(req.Request{
        Method: http.MethodPost,
        URL: "/post",
        Body: HelloRequest{
          Name: "go-req",
        },
    }).Fetch(&out,&r)
    if err != nil {
      panic(err)
    }
    // print go-req
    fmt.Println(out.JSON.Name)
    // print 200
    fmt.Println(r.StatusCode)
}

type HelloRequest struct {
  Name string
}
type HelloResponse struct {
  Name string
}
type PostResponse struct {
  JSON HelloResponse `json:"json"`
}

Used by

Documentation

Index

Examples

Constants

View Source
const RequestContextKey = contextKey("Request")

RequestContextKey for Request instance

Variables

View Source
var FormEncode = Hook{
	Name: "FormEncode",
	OnRequest: func(r *http.Request) error {
		if r.Header.Get("Content-Type") == "" {
			r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		}
		return nil
	},
	Encode: func(ctx context.Context, body any) ([]byte, error) {
		v, err := ValuesOf(body)
		if err != nil {
			return nil, err
		}
		return []byte(v.Encode()), nil
	},
}

FormEncode encode use ValuesOf

View Source
var JSONDecode = Hook{
	Name: "JsonDecode",
	Decode: func(ctx context.Context, body []byte, out any) error {
		return json.Unmarshal(body, out)
	},
}

JSONDecode decode use json.Unmarshal

View Source
var JSONEncode = Hook{
	Name: "JsonEncode",
	OnRequest: func(r *http.Request) error {
		if r.Header.Get("Content-Type") == "" {
			r.Header.Set("Content-Type", "application/json;charset=UTF-8")
		}
		return nil
	},
	Encode: func(ctx context.Context, body any) ([]byte, error) {
		return json.Marshal(body)
	},
}

JSONEncode encode use json.Marshal, add Content-Type

View Source
var MultipartFormEncode = Hook{
	Name: "MultipartFormEncode",
	Encode: func(ctx context.Context, body any) ([]byte, error) {
		buf := &bytes.Buffer{}
		writer := multipart.NewWriter(buf)

		var fieldName, filename string
		var reader io.Reader
		var fields map[string]string

		switch f := body.(type) {
		case *MultipartFile:
			fieldName = f.FieldName
			filename = f.Filename
			reader = f.Reader
			fields = f.Fields
		case fs.File:
			info, err := f.Stat()
			if err != nil {
				return nil, fmt.Errorf("stat file: %w", err)
			}
			filename = info.Name()
			reader = f
		default:
			return nil, fmt.Errorf("MultipartFormEncode: unsupported body type %T, use *MultipartFile or fs.File", body)
		}

		if fieldName == "" {
			fieldName = "file"
		}
		if filename == "" {
			filename = "upload"
		}

		for k, v := range fields {
			if err := writer.WriteField(k, v); err != nil {
				return nil, fmt.Errorf("write field %s: %w", k, err)
			}
		}

		part, err := writer.CreateFormFile(fieldName, filename)
		if err != nil {
			return nil, fmt.Errorf("create form file: %w", err)
		}
		if _, err := io.Copy(part, reader); err != nil {
			return nil, fmt.Errorf("copy file content: %w", err)
		}
		if err := writer.Close(); err != nil {
			return nil, fmt.Errorf("close multipart writer: %w", err)
		}

		return buf.Bytes(), nil
	},
	OnRequest: func(r *http.Request) error {

		if r.Body != nil && r.Header.Get("Content-Type") == "" {

			body, err := io.ReadAll(r.Body)
			if err != nil {
				return err
			}
			if bytes.HasPrefix(body, []byte("--")) {

				idx := bytes.IndexByte(body, '\r')
				if idx < 0 {
					idx = bytes.IndexByte(body, '\n')
				}
				if idx > 2 {
					boundary := string(body[2:idx])
					r.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
				}
			}
			r.Body = io.NopCloser(bytes.NewReader(body))
			r.ContentLength = int64(len(body))
		}
		return nil
	},
}

MultipartFormEncode encodes a MultipartFile or fs.File as multipart/form-data. Body must be *MultipartFile or fs.File.

Functions

func NewContext

func NewContext(ctx context.Context, v *Request) context.Context

NewContext with Request

func ValuesOf

func ValuesOf(v any) (url.Values, error)

ValuesOf convert anything to url.Values

Types

type DebugOptions

type DebugOptions struct {
	Disable   bool                        // Disable turn off debug
	Body      bool                        // Body enable dump http request and response's body
	Out       io.Writer                   // Out debug output, default stderr
	ErrorOnly bool                        // ErrorOnly enable dump error only
	IsError   func(r *http.Response) bool // IsError check if response is error, default is http.StatusOK < 400
}

DebugOptions options for DebugHook

type Extension

type Extension struct {
	Hooks []Hook
}

Extension of Request

func (Extension) Decode

func (e Extension) Decode(ctx context.Context, body []byte, out any) error

Decode body

func (Extension) Encode

func (e Extension) Encode(ctx context.Context, body any) ([]byte, error)

Encode body

func (Extension) HandleOption

func (e Extension) HandleOption(r *Request, o any) (bool, error)

HandleOption process unknown options

func (Extension) OnRequest

func (e Extension) OnRequest(r *http.Request) error

OnRequest process request

func (Extension) OnResponse

func (e Extension) OnResponse(r *http.Response) error

OnResponse process response

func (Extension) RoundTrip

func (e Extension) RoundTrip(r *http.Request) (*http.Response, error)

RoundTrip process request to response

func (*Extension) With

func (e *Extension) With(h ...Hook)

With more hooks

type Hook

type Hook struct {
	Name          string
	Order         int
	OnRequest     func(r *http.Request) error
	OnResponse    func(r *http.Response) error
	HandleRequest func(next http.RoundTripper) http.RoundTripper
	HandleOption  func(r *Request, o any) (bool, error)
	Encode        func(ctx context.Context, body any) ([]byte, error)
	Decode        func(ctx context.Context, body []byte, out any) error
}

Hook phases for Extension

func DebugHook

func DebugHook(o *DebugOptions) Hook

DebugHook dump http.Request and http.Response

func UseRoundTripper

func UseRoundTripper(rt http.RoundTripper) Hook

UseRoundTripper use customized http.RoundTripper for Request

type MultipartFile added in v0.11.0

type MultipartFile struct {
	// FieldName is the form field name (default: "file")
	FieldName string
	// Filename is the filename to use in the multipart header
	Filename string
	// Reader provides the file content
	Reader io.Reader
	// Fields are extra form fields to include
	Fields map[string]string
}

MultipartFile represents a file to be uploaded via multipart form

type Request

type Request struct {
	Method   string
	BaseURL  string
	URL      string
	Query    any
	RawQuery string
	RawBody  []byte
	GetBody  func() (io.ReadCloser, error)
	Body     any
	Header   http.Header
	Context  context.Context

	Values    url.Values // Extra options for customized process - non string option use Context
	LastError error
	// Options support signatures
	//   Request
	//   func(*Request)
	//   func(*Request) error
	//   Hook
	//   nil
	Options   []any
	Extension Extension
}

Request is declarative HTTP client instance

Example
// reusable client
client := req.Request{
	BaseURL: "https://httpbin.org",
	Options: []any{req.JSONEncode, req.JSONDecode},
}

// dump request and response with body
client = client.WithHook(req.DebugHook(&req.DebugOptions{Body: true}))

// send request with declarative override
var out PostResponse
var r *http.Response
err := client.With(req.Request{
	Method: http.MethodPost,
	URL:    "/post",
	Body: HelloRequest{
		Name: "go-req",
	},
}).Fetch(&out, &r)
if err != nil {
	panic(err)
}
// print go-req
fmt.Println(out.JSON.Name)
// print 200
fmt.Println(r.StatusCode)

// override Options use form encode
err = client.With(req.Request{
	Method: http.MethodPost,
	URL:    "/post",
	Body: HelloRequest{
		Name: "go-req",
	},
	Options: []any{req.FormEncode},
}).Fetch(&out)
if err != nil {
	panic(err)
}
// print go-req
fmt.Println(out.Form.Name)

func FromContext

func FromContext(ctx context.Context) *Request

FromContext get Request from context.Context

func (Request) Do

func (r Request) Do() (*http.Response, error)

Do Request

func (Request) Fetch

func (r Request) Fetch(out ...any) error

Fetch decode body

func (Request) FetchBytes

func (r Request) FetchBytes() ([]byte, *http.Response, error)

FetchBytes return bytes

func (Request) FetchString

func (r Request) FetchString() (string, *http.Response, error)

FetchString return string

func (Request) NewRequest

func (r Request) NewRequest() (*http.Request, error)

NewRequest create http.Request

func (*Request) Reconcile

func (r *Request) Reconcile() error

Reconcile apply current options, de-sugar request

func (Request) With

func (r Request) With(o Request) Request

With override Request

func (Request) WithHook

func (r Request) WithHook(h ...Hook) Request

WithHook add Hook to Extension

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL