Skip to content

Commit 1c016ee

Browse files
Adding optional request_timeout and retry attributes (#149)
Co-authored-by: Vincent Chenal <vincent.chenal@protonmail.com>
1 parent 17da36d commit 1c016ee

7 files changed

Lines changed: 502 additions & 179 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'data-source/http: Added `retry` with nested `attempts`, `max_delay` and `min_delay`
3+
([#151](https://github.com/hashicorp/terraform-provider-http/pull/151).'
4+
time: 2023-02-09T16:45:15.060816Z
5+
custom:
6+
Issue: "151"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'data-source/http: Added `request_timeout` ([#151](https://github.com/hashicorp/terraform-provider-http/pull/151).'
3+
time: 2023-02-09T16:46:00.861955Z
4+
custom:
5+
Issue: "151"

docs/data-sources/http.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,22 @@ resource "null_resource" "example" {
148148
- `method` (String) The HTTP Method for the request. Allowed methods are a subset of methods defined in [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3) namely, `GET`, `HEAD`, and `POST`. `POST` support is only intended for read-only URLs, such as submitting a search.
149149
- `request_body` (String) The request body as a string.
150150
- `request_headers` (Map of String) A map of request header field names and values.
151+
- `request_timeout` (Number) The request timeout in milliseconds.
152+
- `retry` (Block, Optional) Retry request configuration. (see [below for nested schema](#nestedblock--retry))
151153

152154
### Read-Only
153155

154156
- `body` (String, Deprecated) The response body returned as a string. **NOTE**: This is deprecated, use `response_body` instead.
155157
- `id` (String) The URL used for the request.
156158
- `response_body` (String) The response body returned as a string.
157159
- `response_headers` (Map of String) A map of response header field names and values. Duplicate headers are concatenated according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
158-
- `status_code` (Number) The HTTP response status code.
160+
- `status_code` (Number) The HTTP response status code.
161+
162+
<a id="nestedblock--retry"></a>
163+
### Nested Schema for `retry`
164+
165+
Optional:
166+
167+
- `attempts` (Number) The number of retry attempts.
168+
- `max_delay` (Number) The maximum delay between retry requests in milliseconds.
169+
- `min_delay` (Number) The minimum delay between retry requests in milliseconds.

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ module github.com/terraform-providers/terraform-provider-http
33
go 1.18
44

55
require (
6+
github.com/hashicorp/go-retryablehttp v0.7.1
67
github.com/hashicorp/terraform-plugin-docs v0.13.0
78
github.com/hashicorp/terraform-plugin-framework v1.1.1
89
github.com/hashicorp/terraform-plugin-framework-validators v0.10.0
910
github.com/hashicorp/terraform-plugin-go v0.14.3
11+
github.com/hashicorp/terraform-plugin-log v0.7.0
1012
github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1
1113
golang.org/x/net v0.5.0
1214
)
@@ -38,7 +40,6 @@ require (
3840
github.com/hashicorp/logutils v1.0.0 // indirect
3941
github.com/hashicorp/terraform-exec v0.17.3 // indirect
4042
github.com/hashicorp/terraform-json v0.14.0 // indirect
41-
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
4243
github.com/hashicorp/terraform-registry-address v0.1.0 // indirect
4344
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
4445
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,16 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
9494
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
9595
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
9696
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
97+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
9798
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
9899
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
99100
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
100101
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
101102
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
102103
github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM=
103104
github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
105+
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
106+
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
104107
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
105108
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
106109
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=

internal/provider/data_source_http.go

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,27 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"errors"
78
"fmt"
89
"io"
910
"mime"
1011
"net/http"
1112
"net/url"
1213
"regexp"
1314
"strings"
15+
"time"
1416

17+
"github.com/hashicorp/go-retryablehttp"
18+
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
1519
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1620
"github.com/hashicorp/terraform-plugin-framework/datasource"
1721
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
1822
"github.com/hashicorp/terraform-plugin-framework/path"
1923
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2024
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
2125
"github.com/hashicorp/terraform-plugin-framework/types"
26+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
27+
"github.com/hashicorp/terraform-plugin-log/tflog"
2228
"golang.org/x/net/http/httpproxy"
2329
)
2430

@@ -90,6 +96,14 @@ your control should be treated as untrustworthy.`,
9096
Optional: true,
9197
},
9298

99+
"request_timeout": schema.Int64Attribute{
100+
Description: "The request timeout in milliseconds.",
101+
Optional: true,
102+
Validators: []validator.Int64{
103+
int64validator.AtLeast(1),
104+
},
105+
},
106+
93107
"response_body": schema.StringAttribute{
94108
Description: "The response body returned as a string.",
95109
Computed: true,
@@ -128,6 +142,36 @@ your control should be treated as untrustworthy.`,
128142
Computed: true,
129143
},
130144
},
145+
146+
Blocks: map[string]schema.Block{
147+
"retry": schema.SingleNestedBlock{
148+
Description: "Retry request configuration.",
149+
Attributes: map[string]schema.Attribute{
150+
"attempts": schema.Int64Attribute{
151+
Description: "The number of retry attempts.",
152+
Optional: true,
153+
Validators: []validator.Int64{
154+
int64validator.AtLeast(0),
155+
},
156+
},
157+
"min_delay": schema.Int64Attribute{
158+
Description: "The minimum delay between retry requests in milliseconds.",
159+
Optional: true,
160+
Validators: []validator.Int64{
161+
int64validator.AtLeast(0),
162+
},
163+
},
164+
"max_delay": schema.Int64Attribute{
165+
Description: "The maximum delay between retry requests in milliseconds.",
166+
Optional: true,
167+
Validators: []validator.Int64{
168+
int64validator.AtLeast(0),
169+
int64validator.AtLeastSumOf(path.MatchRelative().AtParent().AtName("min_delay")),
170+
},
171+
},
172+
},
173+
},
174+
},
131175
}
132176
}
133177

@@ -195,11 +239,38 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
195239
clonedTr.TLSClientConfig.RootCAs = caCertPool
196240
}
197241

198-
client := &http.Client{
199-
Transport: clonedTr,
242+
var retry retryModel
243+
244+
if !model.Retry.IsNull() && !model.Retry.IsUnknown() {
245+
diags = model.Retry.As(ctx, &retry, basetypes.ObjectAsOptions{})
246+
resp.Diagnostics.Append(diags...)
247+
if resp.Diagnostics.HasError() {
248+
return
249+
}
250+
}
251+
252+
retryClient := retryablehttp.NewClient()
253+
retryClient.HTTPClient.Transport = clonedTr
254+
255+
var timeout time.Duration
256+
257+
if model.RequestTimeout.ValueInt64() > 0 {
258+
timeout = time.Duration(model.RequestTimeout.ValueInt64()) * time.Millisecond
259+
retryClient.HTTPClient.Timeout = timeout
260+
}
261+
262+
retryClient.Logger = levelledLogger{ctx}
263+
retryClient.RetryMax = int(retry.Attempts.ValueInt64())
264+
265+
if !retry.MinDelay.IsNull() && !retry.MinDelay.IsUnknown() && retry.MinDelay.ValueInt64() >= 0 {
266+
retryClient.RetryWaitMin = time.Duration(retry.MinDelay.ValueInt64()) * time.Millisecond
267+
}
268+
269+
if !retry.MaxDelay.IsNull() && !retry.MaxDelay.IsUnknown() && retry.MaxDelay.ValueInt64() >= 0 {
270+
retryClient.RetryWaitMax = time.Duration(retry.MaxDelay.ValueInt64()) * time.Millisecond
200271
}
201272

202-
request, err := http.NewRequestWithContext(ctx, method, requestURL, requestBody)
273+
request, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL, requestBody)
203274
if err != nil {
204275
resp.Diagnostics.AddError(
205276
"Error creating request",
@@ -219,8 +290,25 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
219290
request.Header.Set(name, header)
220291
}
221292

222-
response, err := client.Do(request)
293+
response, err := retryClient.Do(request)
223294
if err != nil {
295+
target := &url.Error{}
296+
if errors.As(err, &target) {
297+
if target.Timeout() {
298+
detail := fmt.Sprintf("timeout error: %s", err)
299+
300+
if timeout > 0 {
301+
detail = fmt.Sprintf("request exceeded the specified timeout: %s, err: %s", timeout.String(), err)
302+
}
303+
304+
resp.Diagnostics.AddError(
305+
"Error making request",
306+
detail,
307+
)
308+
return
309+
}
310+
}
311+
224312
resp.Diagnostics.AddError(
225313
"Error making request",
226314
fmt.Sprintf("Error making request: %s", err),
@@ -251,8 +339,7 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
251339

252340
responseHeaders := make(map[string]string)
253341
for k, v := range response.Header {
254-
// Concatenate according to RFC2616
255-
// cf. https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
342+
// Concatenate according to RFC9110 https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2
256343
responseHeaders[k] = strings.Join(v, ", ")
257344
}
258345

@@ -304,10 +391,51 @@ type modelV0 struct {
304391
Method types.String `tfsdk:"method"`
305392
RequestHeaders types.Map `tfsdk:"request_headers"`
306393
RequestBody types.String `tfsdk:"request_body"`
394+
RequestTimeout types.Int64 `tfsdk:"request_timeout"`
395+
Retry types.Object `tfsdk:"retry"`
307396
ResponseHeaders types.Map `tfsdk:"response_headers"`
308397
CaCertificate types.String `tfsdk:"ca_cert_pem"`
309398
Insecure types.Bool `tfsdk:"insecure"`
310399
ResponseBody types.String `tfsdk:"response_body"`
311400
Body types.String `tfsdk:"body"`
312401
StatusCode types.Int64 `tfsdk:"status_code"`
313402
}
403+
404+
type retryModel struct {
405+
Attempts types.Int64 `tfsdk:"attempts"`
406+
MinDelay types.Int64 `tfsdk:"min_delay"`
407+
MaxDelay types.Int64 `tfsdk:"max_delay"`
408+
}
409+
410+
var _ retryablehttp.LeveledLogger = levelledLogger{}
411+
412+
// levelledLogger is used to log messages from retryablehttp.Client to tflog.
413+
type levelledLogger struct {
414+
ctx context.Context
415+
}
416+
417+
func (l levelledLogger) Error(msg string, keysAndValues ...interface{}) {
418+
tflog.Error(l.ctx, msg, l.additionalFields(keysAndValues))
419+
}
420+
421+
func (l levelledLogger) Info(msg string, keysAndValues ...interface{}) {
422+
tflog.Info(l.ctx, msg, l.additionalFields(keysAndValues))
423+
}
424+
425+
func (l levelledLogger) Debug(msg string, keysAndValues ...interface{}) {
426+
tflog.Debug(l.ctx, msg, l.additionalFields(keysAndValues))
427+
}
428+
429+
func (l levelledLogger) Warn(msg string, keysAndValues ...interface{}) {
430+
tflog.Warn(l.ctx, msg, l.additionalFields(keysAndValues))
431+
}
432+
433+
func (l levelledLogger) additionalFields(keysAndValues []interface{}) map[string]interface{} {
434+
additionalFields := make(map[string]interface{}, len(keysAndValues))
435+
436+
for i := 0; i+1 < len(keysAndValues); i += 2 {
437+
additionalFields[fmt.Sprint(keysAndValues[i])] = keysAndValues[i+1]
438+
}
439+
440+
return additionalFields
441+
}

0 commit comments

Comments
 (0)