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

UiPath / uipathcli / 13786392533

11 Mar 2025 10:59AM UTC coverage: 90.225% (+0.1%) from 90.121%
13786392533

push

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.

399 of 419 new or added lines in 22 files covered. (95.23%)

5 existing lines in 3 files now uncovered.

5372 of 5954 relevant lines covered (90.23%)

1.01 hits per line

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

85.32
/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
        "runtime"
14
        "strings"
15

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

26
const NotConfiguredErrorTemplate = `Run config command to set organization and tenant:
27

28
    uipath config
29

30
For more information you can view the help:
31

32
    uipath config --help
33
`
34

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

37
// The HttpExecutor implements the Executor interface and constructs HTTP request
38
// from the given command line parameters and configurations.
39
type HttpExecutor struct {
40
        authenticators []auth.Authenticator
41
}
42

43
func (e HttpExecutor) addHeaders(request *http.Request, headerParameters []ExecutionParameter) {
1✔
44
        converter := converter.NewStringConverter()
1✔
45
        request.Header.Add("User-Agent", UserAgent)
1✔
46
        for _, parameter := range headerParameters {
2✔
47
                headerValue := converter.ToString(parameter.Value)
1✔
48
                request.Header.Add(parameter.Name, headerValue)
1✔
49
        }
1✔
50
}
51

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

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

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

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

123
        result, err := url.Parse(uri)
1✔
124
        if err != nil {
1✔
125
                return nil, fmt.Errorf("Invalid URI '%s': %w", uri, err)
×
126
        }
×
127
        return result, nil
1✔
128
}
129

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

141
func (e HttpExecutor) authenticatorContext(ctx ExecutionContext, request *http.Request) auth.AuthenticatorContext {
1✔
142
        authRequest := *auth.NewAuthenticatorRequest(request.URL.String(), map[string]string{})
1✔
143
        return *auth.NewAuthenticatorContext(
1✔
144
                ctx.AuthConfig.Type,
1✔
145
                ctx.AuthConfig.Config,
1✔
146
                ctx.IdentityUri,
1✔
147
                ctx.Settings.OperationId,
1✔
148
                ctx.Settings.Insecure,
1✔
149
                authRequest)
1✔
150
}
1✔
151

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

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

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

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

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

229
func (e HttpExecutor) writeJsonBody(bodyWriter *io.PipeWriter, parameters []ExecutionParameter, cancel context.CancelCauseFunc) {
1✔
230
        go func() {
2✔
231
                defer bodyWriter.Close()
1✔
232
                err := e.serializeJson(bodyWriter, parameters)
1✔
233
                if err != nil {
1✔
NEW
234
                        cancel(err)
×
235
                        return
×
236
                }
×
237
        }()
238
}
239

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

267
func (e HttpExecutor) pathParameters(ctx ExecutionContext) []ExecutionParameter {
1✔
268
        pathParameters := ctx.Parameters.Path()
1✔
269
        if ctx.Organization != "" {
2✔
270
                pathParameters = append(pathParameters, *NewExecutionParameter("organization", ctx.Organization, "path"))
1✔
271
        }
1✔
272
        if ctx.Tenant != "" {
2✔
273
                pathParameters = append(pathParameters, *NewExecutionParameter("tenant", ctx.Tenant, "path"))
1✔
274
        }
1✔
275
        return pathParameters
1✔
276
}
277

278
func (e HttpExecutor) httpClientSettings(ctx ExecutionContext) network.HttpClientSettings {
1✔
279
        return *network.NewHttpClientSettings(
1✔
280
                ctx.Debug,
1✔
281
                ctx.Settings.OperationId,
1✔
282
                ctx.Settings.Timeout,
1✔
283
                ctx.Settings.MaxAttempts,
1✔
284
                ctx.Settings.Insecure)
1✔
285
}
1✔
286

287
func (e HttpExecutor) Call(ctx ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
1✔
288
        uri, err := e.formatUri(ctx.BaseUri, ctx.Route, e.pathParameters(ctx), ctx.Parameters.Query())
1✔
289
        if err != nil {
2✔
290
                return err
1✔
291
        }
1✔
292
        context, cancel := context.WithCancelCause(context.Background())
1✔
293
        bodyReader, contentType, contentLength, size := e.writeBody(ctx, cancel)
1✔
294
        uploadBar := visualization.NewProgressBar(logger)
1✔
295
        uploadReader := e.progressReader("uploading...", "completing  ", bodyReader, size, uploadBar)
1✔
296
        defer uploadBar.Remove()
1✔
297
        request, err := http.NewRequest(ctx.Method, uri.String(), uploadReader)
1✔
298
        if err != nil {
1✔
299
                return fmt.Errorf("Error preparing request: %w", err)
×
300
        }
×
301
        if contentType != "" {
2✔
302
                request.Header.Add("Content-Type", contentType)
1✔
303
        }
1✔
304
        if contentLength != -1 {
2✔
305
                request.ContentLength = contentLength
1✔
306
        }
1✔
307
        e.addHeaders(request, ctx.Parameters.Header())
1✔
308
        auth, err := e.executeAuthenticators(ctx, request)
1✔
309
        if err != nil {
2✔
310
                return err
1✔
311
        }
1✔
312
        for k, v := range auth.RequestHeader {
2✔
313
                request.Header.Add(k, v)
1✔
314
        }
1✔
315

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

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