github.com/storacha/go-ucanto@v0.7.2/transport/headercar/message/header.go (about)

     1  package message
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  
    10  	"github.com/multiformats/go-multibase"
    11  	"github.com/storacha/go-ucanto/core/car"
    12  	"github.com/storacha/go-ucanto/core/dag/blockstore"
    13  	"github.com/storacha/go-ucanto/core/ipld"
    14  	"github.com/storacha/go-ucanto/core/message"
    15  )
    16  
    17  const (
    18  	// HeaderName is the default name of the HTTP header.
    19  	HeaderName = "X-Agent-Message"
    20  	// Maximum size in bytes the header value may be.
    21  	MaxHeaderSizeBytes = 4 * 1024
    22  )
    23  
    24  var ErrHeaderTooLarge = errors.New("maximum agent message header size exceeded")
    25  
    26  type encodeConfig struct {
    27  	maxSize int
    28  }
    29  
    30  type EncodeOption func(c *encodeConfig)
    31  
    32  // WithMaxSize configures the maximum size allowed for the header value. The
    33  // default is [MaxHeaderSizeBytes]. Set to -1 to disable the size restriction.
    34  func WithMaxSize(size int) EncodeOption {
    35  	return func(c *encodeConfig) {
    36  		c.maxSize = size
    37  	}
    38  }
    39  
    40  // EncodeHeader encodes a [message.AgentMessage] as a HTTP header string.
    41  func EncodeHeader(msg message.AgentMessage, opts ...EncodeOption) (string, error) {
    42  	cfg := encodeConfig{}
    43  	for _, o := range opts {
    44  		o(&cfg)
    45  	}
    46  	if cfg.maxSize == 0 {
    47  		cfg.maxSize = MaxHeaderSizeBytes
    48  	}
    49  
    50  	data := car.Encode([]ipld.Link{msg.Root().Link()}, msg.Blocks())
    51  
    52  	var b bytes.Buffer
    53  	gz := gzip.NewWriter(&b)
    54  	_, err := io.Copy(gz, data)
    55  	if err != nil {
    56  		gz.Close()
    57  		return "", fmt.Errorf("compressing CAR data: %w", err)
    58  	}
    59  	if err := gz.Close(); err != nil {
    60  		return "", fmt.Errorf("closing gzip writer: %w", err)
    61  	}
    62  
    63  	h, err := multibase.Encode(multibase.Base64, b.Bytes())
    64  	if err != nil {
    65  		return "", fmt.Errorf("multibase encoding: %w", err)
    66  	}
    67  
    68  	if cfg.maxSize != -1 && len(h) > cfg.maxSize {
    69  		return "", ErrHeaderTooLarge
    70  	}
    71  
    72  	return h, nil
    73  }
    74  
    75  // DecodeHeader decodes a [message.AgentMessage] from a HTTP header string.
    76  func DecodeHeader(h string) (message.AgentMessage, error) {
    77  	_, data, err := multibase.Decode(h)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("multibase decoding X-Agent-Message header: %w", err)
    80  	}
    81  	gz, err := gzip.NewReader(bytes.NewReader(data))
    82  	if err != nil {
    83  		return nil, fmt.Errorf("creating gzip reader: %w", err)
    84  	}
    85  	defer gz.Close()
    86  	roots, blocks, err := car.Decode(gz)
    87  	if err != nil {
    88  		return nil, fmt.Errorf("decoding CAR: %w", err)
    89  	}
    90  	if len(roots) != 1 {
    91  		return nil, fmt.Errorf("unexpected number of roots: %d", len(roots))
    92  	}
    93  	bstore, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blocks))
    94  	if err != nil {
    95  		return nil, fmt.Errorf("creating blockstore: %w", err)
    96  	}
    97  	return message.NewMessage(roots[0], bstore)
    98  }