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 }