Skip to content

Commit a515737

Browse files
authored
Merge pull request #19 from aoliveti/fix-nopcloser
fix: prevent resource leak in request body & refactor internal naming
2 parents 737e12f + d57e326 commit a515737

1 file changed

Lines changed: 37 additions & 29 deletions

File tree

command.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import (
1212
"strings"
1313
)
1414

15+
type readCloser struct {
16+
io.Reader
17+
io.Closer
18+
}
19+
1520
// A Command represents a cURL command.
1621
type Command struct {
1722
// tokens holds the complete lines of the command.
@@ -20,12 +25,12 @@ type Command struct {
2025
// cfg holds all user-configurable settings.
2126
cfg config
2227

23-
// model is the pre-processed request data used by the builders.
24-
model parsedRequest
28+
// data contains the details extracted from the *http.Request.
29+
data requestData
2530
}
2631

27-
// parsedRequest holds pre-calculated data from the *http.Request.
28-
type parsedRequest struct {
32+
// requestData holds pre-calculated data from the *http.Request.
33+
type requestData struct {
2934
request *http.Request
3035

3136
hasAuth bool
@@ -60,20 +65,19 @@ func NewFromRequest(r *http.Request, opts ...Option) (*Command, error) {
6065
return nil, fmt.Errorf("request url is nil")
6166
}
6267

63-
if err := c.model.build(r, c.cfg); err != nil {
68+
if err := c.data.load(r, c.cfg); err != nil {
6469
return nil, err
6570
}
6671

67-
c.construct()
72+
c.compile()
6873

6974
return &c, nil
7075
}
7176

72-
// build preprocesses the *http.Request into the internal parsedRequest.
73-
// It non-destructively reads (peeks) the request body, sets flags for
74-
// truncation and data presence, and then restores the body so it can be
75-
// read again by subsequent handlers.
76-
func (m *parsedRequest) build(r *http.Request, cfg config) error {
77+
// load extracts relevant data from the *http.Request and populates the internal model.
78+
// It performs a non-destructive read (peek) of the body and restores it,
79+
// ensuring the request remains valid for subsequent handlers.
80+
func (m *requestData) load(r *http.Request, cfg config) error {
7781
m.request = r
7882
m.user, m.pass, m.hasAuth = r.BasicAuth()
7983
// Store the original content length
@@ -124,26 +128,30 @@ func (m *parsedRequest) build(r *http.Request, cfg config) error {
124128
}
125129

126130
// Restore the full request body for subsequent handlers.
127-
r.Body = io.NopCloser(b)
131+
r.Body = &readCloser{
132+
Reader: b,
133+
Closer: r.Body,
134+
}
128135

129136
return nil
130137
}
131138

132-
// construct is the internal orchestrator.
133-
// It runs all the small autonomous builder functions in order.
134-
func (c *Command) construct() {
139+
// compile assembles the final cURL command tokens.
140+
// It executes the specific builder functions (headers, auth, body, etc.)
141+
// and aggregates their output into the final command structure.
142+
func (c *Command) compile() {
135143
// handledHeaders tracks headers handled by builders (e.g., Auth)
136144
handledHeaders := make(map[string]bool)
137145

138146
commandParts := []string{"curl"}
139147
commandParts = buildOptions(commandParts, c.cfg)
140-
commandParts = buildAuth(commandParts, c.cfg, c.model, handledHeaders)
141-
commandParts = buildCookies(commandParts, c.cfg, c.model, handledHeaders)
142-
commandParts = buildData(commandParts, c.cfg, c.model)
143-
commandParts = buildMethod(commandParts, c.cfg, c.model)
144-
commandParts = buildURL(commandParts, c.cfg, c.model)
148+
commandParts = buildAuth(commandParts, c.cfg, c.data, handledHeaders)
149+
commandParts = buildCookies(commandParts, c.cfg, c.data, handledHeaders)
150+
commandParts = buildData(commandParts, c.cfg, c.data)
151+
commandParts = buildMethod(commandParts, c.cfg, c.data)
152+
commandParts = buildURL(commandParts, c.cfg, c.data)
145153

146-
headerParts := buildHeaders(c.cfg, c.model, handledHeaders)
154+
headerParts := buildHeaders(c.cfg, c.data, handledHeaders)
147155

148156
c.tokens = assembleTokens(commandParts, headerParts)
149157
}
@@ -179,8 +187,8 @@ func buildOptions(args []string, cfg config) []string {
179187
return args
180188
}
181189

182-
// buildAuth adds the -u/--user flag and handle the Authorization header.
183-
func buildAuth(args []string, cfg config, model parsedRequest, handledHeaders map[string]bool) []string {
190+
// buildAuth adds the -u/--user flag and handles the Authorization header.
191+
func buildAuth(args []string, cfg config, model requestData, handledHeaders map[string]bool) []string {
184192
if !model.hasAuth {
185193
return args
186194
}
@@ -192,8 +200,8 @@ func buildAuth(args []string, cfg config, model parsedRequest, handledHeaders ma
192200
return args
193201
}
194202

195-
// buildCookies adds the -b/--cookie flag and handle the Cookie header.
196-
func buildCookies(args []string, cfg config, model parsedRequest, handledHeaders map[string]bool) []string {
203+
// buildCookies adds the -b/--cookie flag and handles the Cookie header.
204+
func buildCookies(args []string, cfg config, model requestData, handledHeaders map[string]bool) []string {
197205
if !model.hasCookies {
198206
return args
199207
}
@@ -205,7 +213,7 @@ func buildCookies(args []string, cfg config, model parsedRequest, handledHeaders
205213
}
206214

207215
// buildData adds the --data-raw flag if data exists.
208-
func buildData(args []string, cfg config, model parsedRequest) []string {
216+
func buildData(args []string, cfg config, model requestData) []string {
209217
// We only add the flag if a body was present (even if empty).
210218
if model.body == nil {
211219
return args
@@ -226,7 +234,7 @@ func buildData(args []string, cfg config, model parsedRequest) []string {
226234
}
227235

228236
// buildMethod adds the -X flag if it is not a cURL default.
229-
func buildMethod(args []string, cfg config, model parsedRequest) []string {
237+
func buildMethod(args []string, cfg config, model requestData) []string {
230238
method := model.request.Method
231239
if method == "" {
232240
if model.hasData {
@@ -247,12 +255,12 @@ func buildMethod(args []string, cfg config, model parsedRequest) []string {
247255
}
248256

249257
// buildURL escapes and adds the URL to the end of the main args.
250-
func buildURL(args []string, cfg config, model parsedRequest) []string {
258+
func buildURL(args []string, cfg config, model requestData) []string {
251259
return append(args, escape(cfg.style, model.request.URL.String()))
252260
}
253261

254262
// buildHeaders builds all non-handled HTTP headers.
255-
func buildHeaders(cfg config, model parsedRequest, handledHeaders map[string]bool) []string {
263+
func buildHeaders(cfg config, model requestData, handledHeaders map[string]bool) []string {
256264
r := model.request
257265
if len(r.Header) == 0 && r.Host == "" {
258266
return nil

0 commit comments

Comments
 (0)