• 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

85.83
/executor/http_executor.go
1
package executor
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "errors"
8
        "fmt"
9
        "io"
10
        "mime/multipart"
11
        "net/http"
12
        "net/url"
13
        "strings"
14

15
        "github.com/UiPath/uipathcli/auth"
16
        "github.com/UiPath/uipathcli/log"
17
        "github.com/UiPath/uipathcli/output"
18
        "github.com/UiPath/uipathcli/utils/converter"
19
        "github.com/UiPath/uipathcli/utils/network"
20
        "github.com/UiPath/uipathcli/utils/stream"
21
        "github.com/UiPath/uipathcli/utils/visualization"
22
)
23

24
const NotConfiguredErrorTemplate = `Run config command to set organization and tenant:
25

26
    uipath config
27

28
For more information you can view the help:
29

30
    uipath config --help
31
`
32

33
// The HttpExecutor implements the Executor interface and constructs HTTP request
34
// from the given command line parameters and configurations.
35
type HttpExecutor struct {
36
        authenticators []auth.Authenticator
37
}
38

39
func (e HttpExecutor) addHeaders(header http.Header, headerParameters []ExecutionParameter) {
1✔
40
        converter := converter.NewStringConverter()
1✔
41
        for _, parameter := range headerParameters {
2✔
42
                headerValue := converter.ToString(parameter.Value)
1✔
43
                header.Set(parameter.Name, headerValue)
1✔
44
        }
1✔
45
}
46

47
func (e HttpExecutor) calculateMultipartSize(parameters []ExecutionParameter) int64 {
1✔
48
        result := int64(0)
1✔
49
        for _, parameter := range parameters {
2✔
50
                switch v := parameter.Value.(type) {
1✔
51
                case string:
×
52
                        result = result + int64(len(v))
×
53
                case stream.Stream:
1✔
54
                        size, err := v.Size()
1✔
55
                        if err == nil {
2✔
56
                                result = result + size
1✔
57
                        }
1✔
58
                }
59
        }
60
        return result
1✔
61
}
62

63
func (e HttpExecutor) writeMultipartForm(writer *multipart.Writer, parameters []ExecutionParameter) error {
1✔
64
        for _, parameter := range parameters {
2✔
65
                switch v := parameter.Value.(type) {
1✔
66
                case string:
×
67
                        w, err := writer.CreateFormField(parameter.Name)
×
68
                        if err != nil {
×
69
                                return fmt.Errorf("Error creating form field '%s': %w", parameter.Name, err)
×
70
                        }
×
71
                        _, err = w.Write([]byte(v))
×
72
                        if err != nil {
×
73
                                return fmt.Errorf("Error writing form field '%s': %w", parameter.Name, err)
×
74
                        }
×
75
                case stream.Stream:
1✔
76
                        w, err := writer.CreateFormFile(parameter.Name, v.Name())
1✔
77
                        if err != nil {
1✔
78
                                return fmt.Errorf("Error writing form file '%s': %w", parameter.Name, err)
×
79
                        }
×
80
                        data, err := v.Data()
1✔
81
                        if err != nil {
2✔
82
                                return err
1✔
83
                        }
1✔
84
                        defer data.Close()
1✔
85
                        _, err = io.Copy(w, data)
1✔
86
                        if err != nil {
1✔
87
                                return fmt.Errorf("Error writing form file '%s': %w", parameter.Name, err)
×
88
                        }
×
89
                }
90
        }
91
        return nil
1✔
92
}
93

94
func (e HttpExecutor) serializeJson(body io.Writer, parameters []ExecutionParameter) error {
1✔
95
        data := map[string]interface{}{}
1✔
96
        for _, parameter := range parameters {
2✔
97
                data[parameter.Name] = parameter.Value
1✔
98
        }
1✔
99
        result, err := json.Marshal(data)
1✔
100
        if err != nil {
1✔
101
                return fmt.Errorf("Error creating body: %w", err)
×
102
        }
×
103
        _, err = body.Write(result)
1✔
104
        if err != nil {
1✔
105
                return fmt.Errorf("Error writing body: %w", err)
×
106
        }
×
107
        return nil
1✔
108
}
109

110
func (e HttpExecutor) validateUri(uri string) (*url.URL, error) {
1✔
111
        if strings.Contains(uri, "{organization}") {
2✔
112
                return nil, fmt.Errorf("Missing organization parameter!\n\n%s", NotConfiguredErrorTemplate)
1✔
113
        }
1✔
114
        if strings.Contains(uri, "{tenant}") {
2✔
115
                return nil, fmt.Errorf("Missing tenant parameter!\n\n%s", NotConfiguredErrorTemplate)
1✔
116
        }
1✔
117

118
        result, err := url.Parse(uri)
1✔
119
        if err != nil {
1✔
120
                return nil, fmt.Errorf("Invalid URI '%s': %w", uri, err)
×
121
        }
×
122
        return result, nil
1✔
123
}
124

125
func (e HttpExecutor) formatUri(baseUri url.URL, route string, pathParameters []ExecutionParameter, queryParameters []ExecutionParameter) (*url.URL, error) {
1✔
126
        uriBuilder := converter.NewUriBuilder(baseUri, route)
1✔
127
        for _, parameter := range pathParameters {
2✔
128
                uriBuilder.FormatPath(parameter.Name, parameter.Value)
1✔
129
        }
1✔
130
        for _, parameter := range queryParameters {
2✔
131
                uriBuilder.AddQueryString(parameter.Name, parameter.Value)
1✔
132
        }
1✔
133
        return e.validateUri(uriBuilder.Build())
1✔
134
}
135

136
func (e HttpExecutor) authenticatorContext(ctx ExecutionContext, url string) auth.AuthenticatorContext {
1✔
137
        authRequest := *auth.NewAuthenticatorRequest(url, map[string]string{})
1✔
138
        return *auth.NewAuthenticatorContext(
1✔
139
                ctx.AuthConfig.Type,
1✔
140
                ctx.AuthConfig.Config,
1✔
141
                ctx.IdentityUri,
1✔
142
                ctx.Settings.OperationId,
1✔
143
                ctx.Settings.Insecure,
1✔
144
                authRequest)
1✔
145
}
1✔
146

147
func (e HttpExecutor) executeAuthenticators(ctx ExecutionContext, url string) (*auth.AuthenticatorResult, error) {
1✔
148
        authContext := e.authenticatorContext(ctx, url)
1✔
149
        for _, authProvider := range e.authenticators {
2✔
150
                result := authProvider.Auth(authContext)
1✔
151
                if result.Error != "" {
2✔
152
                        return nil, errors.New(result.Error)
1✔
153
                }
1✔
154
                authContext.Config = result.Config
1✔
155
                for k, v := range result.RequestHeader {
2✔
156
                        authContext.Request.Header[k] = v
1✔
157
                }
1✔
158
        }
159
        return auth.AuthenticatorSuccess(authContext.Request.Header, authContext.Config), nil
1✔
160
}
161

162
func (e HttpExecutor) progressReader(text string, completedText string, reader io.Reader, length int64, progressBar *visualization.ProgressBar) io.Reader {
1✔
163
        if length < 10*1024*1024 {
2✔
164
                return reader
1✔
165
        }
1✔
166
        return visualization.NewProgressReader(reader, func(progress visualization.Progress) {
2✔
167
                displayText := text
1✔
168
                if progress.Completed {
2✔
169
                        displayText = completedText
1✔
170
                }
1✔
171
                progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
1✔
172
        })
173
}
174

175
func (e HttpExecutor) writeMultipartBody(bodyWriter *io.PipeWriter, parameters []ExecutionParameter, cancel context.CancelCauseFunc) (string, int64) {
1✔
176
        multipartSize := e.calculateMultipartSize(parameters)
1✔
177
        formWriter := multipart.NewWriter(bodyWriter)
1✔
178
        go func() {
2✔
179
                defer bodyWriter.Close()
1✔
180
                defer formWriter.Close()
1✔
181
                err := e.writeMultipartForm(formWriter, parameters)
1✔
182
                if err != nil {
2✔
183
                        cancel(err)
1✔
184
                        return
1✔
185
                }
1✔
186
        }()
187
        return formWriter.FormDataContentType(), multipartSize
1✔
188
}
189

190
func (e HttpExecutor) writeInputBody(bodyWriter *io.PipeWriter, input stream.Stream, cancel context.CancelCauseFunc) {
1✔
191
        go func() {
2✔
192
                defer bodyWriter.Close()
1✔
193
                data, err := input.Data()
1✔
194
                if err != nil {
1✔
NEW
195
                        cancel(err)
×
196
                        return
×
197
                }
×
198
                defer data.Close()
1✔
199
                _, err = io.Copy(bodyWriter, data)
1✔
200
                if err != nil {
1✔
NEW
201
                        cancel(err)
×
202
                        return
×
203
                }
×
204
        }()
205
}
206

207
func (e HttpExecutor) writeUrlEncodedBody(bodyWriter *io.PipeWriter, parameters []ExecutionParameter, cancel context.CancelCauseFunc) {
1✔
208
        go func() {
2✔
209
                defer bodyWriter.Close()
1✔
210
                queryStringBuilder := converter.NewQueryStringBuilder()
1✔
211
                for _, parameter := range parameters {
2✔
212
                        queryStringBuilder.Add(parameter.Name, parameter.Value)
1✔
213
                }
1✔
214
                queryString := queryStringBuilder.Build()
1✔
215
                _, err := bodyWriter.Write([]byte(queryString))
1✔
216
                if err != nil {
1✔
NEW
217
                        cancel(err)
×
218
                        return
×
219
                }
×
220
        }()
221
}
222

223
func (e HttpExecutor) writeJsonBody(bodyWriter *io.PipeWriter, parameters []ExecutionParameter, cancel context.CancelCauseFunc) {
1✔
224
        go func() {
2✔
225
                defer bodyWriter.Close()
1✔
226
                err := e.serializeJson(bodyWriter, parameters)
1✔
227
                if err != nil {
1✔
NEW
228
                        cancel(err)
×
229
                        return
×
230
                }
×
231
        }()
232
}
233

234
func (e HttpExecutor) writeBody(ctx ExecutionContext, cancel context.CancelCauseFunc) (io.ReadCloser, string, int64, int64) {
1✔
235
        if ctx.Input != nil {
2✔
236
                reader, writer := io.Pipe()
1✔
237
                e.writeInputBody(writer, ctx.Input, cancel)
1✔
238
                contentLength, _ := ctx.Input.Size()
1✔
239
                return reader, ctx.ContentType, contentLength, contentLength
1✔
240
        }
1✔
241
        formParameters := ctx.Parameters.Form()
1✔
242
        if len(formParameters) > 0 {
2✔
243
                reader, writer := io.Pipe()
1✔
244
                contentType, multipartSize := e.writeMultipartBody(writer, formParameters, cancel)
1✔
245
                return reader, contentType, -1, multipartSize
1✔
246
        }
1✔
247
        bodyParameters := ctx.Parameters.Body()
1✔
248
        if len(bodyParameters) > 0 && ctx.ContentType == "application/x-www-form-urlencoded" {
2✔
249
                reader, writer := io.Pipe()
1✔
250
                e.writeUrlEncodedBody(writer, bodyParameters, cancel)
1✔
251
                return reader, ctx.ContentType, -1, -1
1✔
252
        }
1✔
253
        if len(bodyParameters) > 0 {
2✔
254
                reader, writer := io.Pipe()
1✔
255
                e.writeJsonBody(writer, bodyParameters, cancel)
1✔
256
                return reader, ctx.ContentType, -1, -1
1✔
257
        }
1✔
258
        return io.NopCloser(bytes.NewReader([]byte{})), ctx.ContentType, -1, -1
1✔
259
}
260

261
func (e HttpExecutor) pathParameters(ctx ExecutionContext) []ExecutionParameter {
1✔
262
        pathParameters := ctx.Parameters.Path()
1✔
263
        if ctx.Organization != "" {
2✔
264
                pathParameters = append(pathParameters, *NewExecutionParameter("organization", ctx.Organization, "path"))
1✔
265
        }
1✔
266
        if ctx.Tenant != "" {
2✔
267
                pathParameters = append(pathParameters, *NewExecutionParameter("tenant", ctx.Tenant, "path"))
1✔
268
        }
1✔
269
        return pathParameters
1✔
270
}
271

272
func (e HttpExecutor) httpClientSettings(ctx ExecutionContext) network.HttpClientSettings {
1✔
273
        return *network.NewHttpClientSettings(
1✔
274
                ctx.Debug,
1✔
275
                ctx.Settings.OperationId,
1✔
276
                ctx.Settings.Timeout,
1✔
277
                ctx.Settings.MaxAttempts,
1✔
278
                ctx.Settings.Insecure)
1✔
279
}
1✔
280

281
func (e HttpExecutor) Call(ctx ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
282
        uri, err := e.formatUri(ctx.BaseUri, ctx.Route, e.pathParameters(ctx), ctx.Parameters.Query())
1✔
283
        if err != nil {
2✔
284
                return err
1✔
285
        }
1✔
286
        context, cancel := context.WithCancelCause(context.Background())
1✔
287
        bodyReader, contentType, contentLength, size := e.writeBody(ctx, cancel)
1✔
288
        uploadBar := visualization.NewProgressBar(logger)
1✔
289
        uploadReader := e.progressReader("uploading...", "completing  ", bodyReader, size, uploadBar)
1✔
290
        defer uploadBar.Remove()
1✔
291

1✔
292
        auth, err := e.executeAuthenticators(ctx, uri.String())
1✔
293
        if err != nil {
2✔
294
                return err
1✔
295
        }
1✔
296

297
        header := http.Header{}
1✔
298
        if contentType != "" {
2✔
299
                header.Set("Content-Type", contentType)
1✔
300
        }
1✔
301
        e.addHeaders(header, ctx.Parameters.Header())
1✔
302
        for k, v := range auth.RequestHeader {
2✔
303
                header.Set(k, v)
1✔
304
        }
1✔
305
        request := network.NewHttpRequest(ctx.Method, uri.String(), header, uploadReader, contentLength)
1✔
306

1✔
307
        client := network.NewHttpClient(logger, e.httpClientSettings(ctx))
1✔
308
        response, err := client.SendWithContext(request, context)
1✔
309
        if err != nil {
2✔
310
                return err
1✔
311
        }
1✔
312
        defer response.Body.Close()
1✔
313
        downloadBar := visualization.NewProgressBar(logger)
1✔
314
        downloadReader := e.progressReader("downloading...", "completing    ", response.Body, response.ContentLength, downloadBar)
1✔
315
        defer downloadBar.Remove()
1✔
316
        body, err := io.ReadAll(downloadReader)
1✔
317
        if err != nil {
1✔
NEW
318
                return fmt.Errorf("Error reading response body: %w", err)
×
319
        }
×
320
        err = writer.WriteResponse(*output.NewResponseInfo(response.StatusCode, response.Status, response.Proto, response.Header, bytes.NewReader(body)))
1✔
321
        if err != nil {
2✔
322
                return err
1✔
323
        }
1✔
324
        return nil
1✔
325
}
326

327
func NewHttpExecutor(authenticators []auth.Authenticator) *HttpExecutor {
1✔
328
        return &HttpExecutor{authenticators}
1✔
329
}
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

© 2025 Coveralls, Inc