• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

UiPath / uipathcli / 13829663743

13 Mar 2025 07:55AM UTC coverage: 90.604% (+0.5%) from 90.121%
13829663743

Pull #156

github

thschmitt
Add network package with common http client

Moved common code for handling HTTP requests in a new shared
network package to avoid duplication and ensure that each plugin
handles network requests the same way.

The common HTTP client code takes care of a couple of cross-cutting
concerns needed by all plugins and the main HTTP executor:

- Logs request and response when debug flag is set. Added a new
  resettableReader which preserves the request and response bodies while
  they are being read and forwards them to the debug logger.

- Added retries for all HTTP requests. This also leverages the
  resettableReader to ensure the request body can be replayed.

- The `CommandBuilder` generates now an operation id which is set on every
  request the uipathcli performs. The same operation id is kept for the
  duration of the whole command execution which makes it easier to
  correlate multiple requests performed by a single command.

- The `HttpClient` also sets transport-related settings like certificate
  validation and response header timeout.

- Using the built-in context instead of a custom requestError channel.
Pull Request #156: Add network package with common http client

478 of 505 new or added lines in 26 files covered. (94.65%)

8 existing lines in 5 files now uncovered.

5371 of 5928 relevant lines covered (90.6%)

1.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/utils/network/http_client.go
1
package network
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/tls"
7
        "fmt"
8
        "io"
9
        "net/http"
10
        "runtime"
11

12
        "github.com/UiPath/uipathcli/log"
13
        "github.com/UiPath/uipathcli/utils"
14
        "github.com/UiPath/uipathcli/utils/resiliency"
15
)
16

17
type HttpClient struct {
18
        logger   log.Logger
19
        settings HttpClientSettings
20
}
21

22
const bufferLimit = 10 * 1024 * 1024
23
const loggingLimit = 1 * 1024 * 1024
24

25
var UserAgent = fmt.Sprintf("uipathcli/%s (%s; %s)", utils.Version, runtime.GOOS, runtime.GOARCH)
26

27
func (c HttpClient) Send(request *HttpRequest) (*HttpResponse, error) {
1✔
28
        return c.sendWithRetries(request, context.Background())
1✔
29
}
1✔
30

31
func (c HttpClient) SendWithContext(request *HttpRequest, ctx context.Context) (*HttpResponse, error) {
1✔
32
        return c.sendWithRetries(request, ctx)
1✔
33
}
1✔
34

35
func (c HttpClient) sendWithRetries(request *HttpRequest, ctx context.Context) (*HttpResponse, error) {
1✔
36
        request.Header.Set("User-Agent", UserAgent)
1✔
37
        request.Header.Set("x-request-id", c.settings.OperationId)
1✔
38

1✔
39
        if c.settings.Debug {
2✔
40
                request.Body = newResettableReader(request.Body, bufferLimit, func(body []byte) { c.logRequest(request, body) })
2✔
41
        } else if c.settings.MaxAttempts > 1 {
2✔
42
                request.Body = newResettableReader(request.Body, bufferLimit, func(body []byte) {})
2✔
43
        }
44

45
        var response *HttpResponse
1✔
46
        var err error
1✔
47
        err = resiliency.RetryN(c.settings.MaxAttempts, func(attempt int) error {
2✔
48
                if attempt > 1 && !c.resetReader(request.Body) {
1✔
NEW
49
                        return err
×
NEW
50
                }
×
51

52
                response, err = c.send(request, ctx)
1✔
53
                if err != nil {
2✔
54
                        return resiliency.Retryable(err)
1✔
55
                }
1✔
56

57
                if c.settings.Debug {
2✔
58
                        response.Body = newResettableReader(response.Body, bufferLimit, func(body []byte) { c.logResponse(response, body) })
2✔
59
                }
60

61
                if response.StatusCode == 0 || response.StatusCode >= 500 {
2✔
62
                        defer response.Body.Close()
1✔
63
                        body, err := io.ReadAll(response.Body)
1✔
64
                        if err != nil {
1✔
NEW
65
                                return resiliency.Retryable(fmt.Errorf("Error reading response: %w", err))
×
NEW
66
                        }
×
67
                        return resiliency.Retryable(fmt.Errorf("Service returned status code '%v' and body '%v'", response.StatusCode, string(body)))
1✔
68
                }
69
                return nil
1✔
70
        })
71
        return response, err
1✔
72
}
73

74
func (c HttpClient) resetReader(reader io.Reader) bool {
1✔
75
        resettableReader, ok := reader.(*resettableReader)
1✔
76
        if ok {
2✔
77
                return resettableReader.Reset()
1✔
78
        }
1✔
NEW
79
        return false
×
80
}
81

82
func (c HttpClient) send(request *HttpRequest, ctx context.Context) (*HttpResponse, error) {
1✔
83
        transport := &http.Transport{
1✔
84
                TLSClientConfig:       &tls.Config{InsecureSkipVerify: c.settings.Insecure}, //nolint // This is user configurable and disabled by default
1✔
85
                ResponseHeaderTimeout: c.settings.Timeout,
1✔
86
        }
1✔
87
        client := &http.Client{Transport: transport}
1✔
88

1✔
89
        responseChan := make(chan *HttpResponse)
1✔
90
        ctx, cancel := context.WithCancelCause(ctx)
1✔
91
        go func(client *http.Client, request *HttpRequest) {
2✔
92
                req, err := http.NewRequest(request.Method, request.URL, request.Body)
1✔
93
                if err != nil {
1✔
NEW
94
                        cancel(fmt.Errorf("Error preparing request: %w", err))
×
NEW
95
                        return
×
NEW
96
                }
×
97
                req.Header = request.Header
1✔
98
                req.ContentLength = request.ContentLength
1✔
99

1✔
100
                resp, err := client.Do(req)
1✔
101
                if err != nil {
2✔
102
                        cancel(fmt.Errorf("Error sending request: %w", err))
1✔
103
                        return
1✔
104
                }
1✔
105

106
                response := NewHttpResponse(
1✔
107
                        resp.Status,
1✔
108
                        resp.StatusCode,
1✔
109
                        resp.Proto,
1✔
110
                        resp.Header,
1✔
111
                        resp.Body,
1✔
112
                        resp.ContentLength)
1✔
113
                responseChan <- response
1✔
114
        }(client, request)
115

116
        select {
1✔
117
        case <-ctx.Done():
1✔
118
                return nil, fmt.Errorf("Error sending request: %w", context.Cause(ctx))
1✔
119
        case response := <-responseChan:
1✔
120
                return response, nil
1✔
121
        }
122
}
123

124
func (c HttpClient) logRequest(request *HttpRequest, body []byte) {
1✔
125
        reader := bytes.NewReader(c.truncate(body, loggingLimit))
1✔
126
        requestInfo := log.NewRequestInfo(request.Method, request.URL, request.Proto, request.Header, reader)
1✔
127
        c.logger.LogRequest(*requestInfo)
1✔
128
}
1✔
129

130
func (c HttpClient) logResponse(response *HttpResponse, body []byte) {
1✔
131
        reader := bytes.NewReader(c.truncate(body, loggingLimit))
1✔
132
        responseInfo := log.NewResponseInfo(response.StatusCode, response.Status, response.Proto, response.Header, reader)
1✔
133
        c.logger.LogResponse(*responseInfo)
1✔
134
}
1✔
135

136
func (c HttpClient) truncate(data []byte, size int) []byte {
1✔
137
        if len(data) > size {
1✔
NEW
138
                return data[:size]
×
NEW
139
        }
×
140
        return data
1✔
141
}
142

143
func NewHttpClient(logger log.Logger, settings HttpClientSettings) *HttpClient {
1✔
144
        return &HttpClient{logger, settings}
1✔
145
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc