|
| 1 | +--- |
| 2 | +page_title: "File and Multipart Request Bodies" |
| 3 | +subcategory: "Guides" |
| 4 | +description: |- |
| 5 | + Load request bodies from disk with request_body_file and build multipart/form-data payloads with request_multipart across TerraCurl surfaces. |
| 6 | +--- |
| 7 | + |
| 8 | +# File and Multipart Request Bodies |
| 9 | + |
| 10 | +TerraCurl supports two complementary ways to send non-JSON request bodies without embedding large or binary payloads in Terraform configuration or state: |
| 11 | + |
| 12 | +| Mechanism | Use case | Stored in state | |
| 13 | +|-----------|----------|-----------------| |
| 14 | +| `request_body_file` | Raw binary or text body (for example a ZIP deploy) | File path only | |
| 15 | +| `request_multipart` | `multipart/form-data` with text fields and file parts | Part metadata and paths only | |
| 16 | + |
| 17 | +These attributes are available on **resources**, **data sources**, **actions**, and **ephemeral resources**. On `terracurl_request` resources, matching `read_*` and `destroy_*` variants exist for read and destroy operations. |
| 18 | + |
| 19 | +## Mutual exclusion |
| 20 | + |
| 21 | +For each HTTP operation, configure **one** body source: |
| 22 | + |
| 23 | +- `request_body` or `request_body_wo` (resources only for `_wo`) |
| 24 | +- `request_body_file` |
| 25 | +- `request_multipart` |
| 26 | + |
| 27 | +The provider validates this at plan time. |
| 28 | + |
| 29 | +## File-based request bodies |
| 30 | + |
| 31 | +Use `request_body_file` when the API expects a raw payload such as a ZIP archive, protobuf message, or plain-text document. |
| 32 | + |
| 33 | +```terraform |
| 34 | +resource "terracurl_request" "kudu_deploy" { |
| 35 | + name = "kudu-deploy" |
| 36 | + url = "https://myapp.scm.azurewebsites.net/api/zipdeploy?isAsync=true" |
| 37 | + method = "PUT" |
| 38 | +
|
| 39 | + request_body_file = "${path.module}/app.zip" |
| 40 | +
|
| 41 | + headers = { |
| 42 | + Content-Type = "application/zip" |
| 43 | + } |
| 44 | +
|
| 45 | + response_codes = [200, 202] |
| 46 | + skip_read = true |
| 47 | + skip_destroy = true |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +Behavior: |
| 52 | + |
| 53 | +- The provider reads the file with `os.ReadFile` at HTTP call time. |
| 54 | +- Only the **path** is stored in Terraform state, not the file bytes (same pattern as `cert_file`). |
| 55 | +- Changing the path triggers replace via `RequiresReplace`. |
| 56 | +- Content changes at the same path do **not** automatically trigger replace. Taint or change the path when the on-disk file changes. |
| 57 | + |
| 58 | +Set `Content-Type` in `headers` when the API requires a specific media type. |
| 59 | + |
| 60 | +## Multipart form bodies |
| 61 | + |
| 62 | +Use `request_multipart` for APIs that accept `multipart/form-data` uploads (similar to `curl -F name=John -F photo=@john.jpg`). |
| 63 | + |
| 64 | +```terraform |
| 65 | +resource "terracurl_request" "upload" { |
| 66 | + name = "upload" |
| 67 | + url = "https://api.example.com/upload" |
| 68 | + method = "POST" |
| 69 | +
|
| 70 | + request_multipart = { |
| 71 | + parts = [ |
| 72 | + { name = "name", value = "John" }, |
| 73 | + { name = "photo", file_path = "${path.module}/john.jpg", content_type = "image/jpeg" }, |
| 74 | + ] |
| 75 | + } |
| 76 | +
|
| 77 | + response_codes = [200] |
| 78 | + skip_read = true |
| 79 | + skip_destroy = true |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Each part requires: |
| 84 | + |
| 85 | +| Attribute | Required | Description | |
| 86 | +|-----------|----------|-------------| |
| 87 | +| `name` | Yes | Form field name | |
| 88 | +| `value` | One of `value` or `file_path` | Text field value | |
| 89 | +| `file_path` | One of `value` or `file_path` | Path to a file on disk | |
| 90 | +| `content_type` | No | Defaults to `text/plain` for value parts and `application/octet-stream` for file parts | |
| 91 | + |
| 92 | +The provider builds the body with Go's `mime/multipart` package and sets `Content-Type: multipart/form-data; boundary=...` on the outbound request. If you also set `Content-Type` in `headers`, the provider-generated multipart header **wins** for that operation. |
| 93 | + |
| 94 | +## Data source example |
| 95 | + |
| 96 | +```terraform |
| 97 | +data "terracurl_request" "upload_status" { |
| 98 | + name = "upload-status" |
| 99 | + url = "https://api.example.com/upload" |
| 100 | + method = "POST" |
| 101 | + response_codes = ["200"] |
| 102 | +
|
| 103 | + request_multipart = { |
| 104 | + parts = [ |
| 105 | + { name = "name", value = "John" }, |
| 106 | + { name = "photo", file_path = "${path.module}/john.jpg" }, |
| 107 | + ] |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +## Action example |
| 113 | + |
| 114 | +```terraform |
| 115 | +action "terracurl_request" "notify" { |
| 116 | + url = "https://api.example.com/notify" |
| 117 | + method = "POST" |
| 118 | +
|
| 119 | + request_body_file = "${path.module}/payload.bin" |
| 120 | +
|
| 121 | + headers = { |
| 122 | + Content-Type = "application/octet-stream" |
| 123 | + } |
| 124 | +
|
| 125 | + response_codes = [200, 204] |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +## Ephemeral resource example |
| 130 | + |
| 131 | +Ephemeral resources support `request_body_file` / `request_multipart` on open, plus `renew_*` and `close_*` variants. Renew and close file paths and multipart metadata are snapshotted to private state at open and re-read from disk at renew/close time. |
| 132 | + |
| 133 | +```terraform |
| 134 | +ephemeral "terracurl_request" "session" { |
| 135 | + name = "session" |
| 136 | + url = "https://api.example.com/open" |
| 137 | + method = "POST" |
| 138 | + response_codes = ["201"] |
| 139 | +
|
| 140 | + skip_renew = false |
| 141 | + renew_url = "https://api.example.com/renew" |
| 142 | + renew_method = "PUT" |
| 143 | + renew_request_body_file = "${path.module}/renew-payload.json" |
| 144 | + renew_response_codes = ["200"] |
| 145 | +
|
| 146 | + skip_close = false |
| 147 | + close_url = "https://api.example.com/close" |
| 148 | + close_method = "DELETE" |
| 149 | + close_request_multipart = { |
| 150 | + parts = [ |
| 151 | + { name = "token", value = "placeholder" }, |
| 152 | + ] |
| 153 | + } |
| 154 | + close_response_codes = ["204"] |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +## Resource read and destroy variants |
| 159 | + |
| 160 | +On `terracurl_request` resources: |
| 161 | + |
| 162 | +- `read_request_body_file` / `read_request_multipart` for read calls |
| 163 | +- `destroy_request_body_file` / `destroy_request_multipart` for destroy calls |
| 164 | + |
| 165 | +Write-only string bodies (`*_request_body_wo`) remain available for inline secrets. File and multipart attributes are already path/metadata-only in state, so write-only variants are not provided for them. |
| 166 | + |
| 167 | +## Destroy templating with multipart |
| 168 | + |
| 169 | +`{response.<path>}` placeholders in `destroy_request_multipart` part `value` fields are substituted at destroy time from the stored create response. File parts (`file_path`) are not templated. |
| 170 | + |
| 171 | +See the [Destroy Response Templating guide](destroy_templating) for placeholder syntax. |
| 172 | + |
| 173 | +## Limitations |
| 174 | + |
| 175 | +- File contents are loaded fully into memory at request time (same as embedding bytes in configuration). |
| 176 | +- The provider does not sniff MIME types from file extensions; set `content_type` explicitly when needed. |
| 177 | +- Write-only multipart parts are not supported (multipart configuration is already metadata-only in state). |
| 178 | + |
| 179 | +## Related guides |
| 180 | + |
| 181 | +- [Write-Only Headers and Request Bodies](write_only) — inline secret bodies without persisting values in state |
| 182 | +- [Destroy Response Templating](destroy_templating) — inject create response values into destroy configuration |
0 commit comments