Microvisor provides a set of System Calls which allow your application to initiate HTTP requests to Internet-hosted resources and work with the data they return. As with all Microvisor tasks, this is a transactional process: your code tells Microvisor which resource to retrieve, and Microvisor responds with the requested data or a reason why it was unable to gather it.
This guide will walk you through this process to show you how your application works with Microvisor to make and manage HTTP requests.
Microvisor's HTTP communication system is built upon its core networking system. In other words, you ask Microvisor to establish a network connection and then to open a data-transfer channel through which HTTP requests and the responses they generate will flow. For this reason, you should review Microvisor Networking before continuing. Here we'll assume that you know how to set up networking and notifications, and work with channels.
The code used in this guide is available in full as a buildable application.
HTTP communications involves using the specific channel type MV_CHANNELTYPE_HTTP
. You will use this channel type when you call mvOpenChannel()
and pass in a reference to a standard MvOpenChannelParams
structure as described in Microvisor Networking and the Network System Calls documentation.
HTTP channels' send and receive buffers must be sized to a multiple of 512 bytes and their addresses must be 512-byte aligned. For example:
1// Set up the HTTP channel's send and receive buffers2static volatile uint8_t http_channel_rx_buffer[1536] __attribute__((aligned(512)));3static volatile uint8_t http_channel_tx_buffer[512] __attribute__((aligned(512)));
You may want to make a manual curl
request to the target server to gauge the length of a typical response when choosing your RX buffer size.
When sizing your HTTP receive buffers, ensure you allow sufficient space for not only the response's body but also its headers. If the received data is too large for your choice of buffer size, the value of the result
property of the MvHttpResponseData
record returned when you call mvReadHttpResponseData()
— as shown in Managing request outcomes, below — will be MV_HTTPRESULT_RESPONSETOOLARGE
. You need to close your channel, establish a new one with a large RX buffer, and re-issue your HTTP request.
To ensure that a response is bound to the request that initiated it, each HTTP channel supports just one request. Whether an issued request succeeds or fails — and what we mean by 'issued' is discussed in a moment — its host channel can't be used for any further requests, even post-failure re-sends. Instead, you must close the channel and then open a new one. However, you can use the same MvOpenChannelParams
and MvHttpRequest
structures that you used earlier, just updated with the new channel's handle.
Issuing an HTTP request is a two-stage process:
If Microvisor rejects the request, perhaps because one of the request record's parameters is malformed, or the request record is too large for the channel's send buffer, then the request is not considered to be issued, and the channel can be used to re-issue a fresh request. The rejection is indicated by a non-zero return value from the System Call used to issue the request to Microvisor, mvSendHttpRequest()
.
If Microvisor accepts a request record — so mvSendHttpRequest()
returns MV_STATUS_OKAY
— the channel can't be used for any further requests, and Microvisor will return MV_STATUS_REQUESTALREADYSENT
to all subsequent mvSendHttpRequest()
calls that target the same channel.
Accepted requests can still fail to be sent. For example, the target server's domain may fail to resolve, or you may have specified an unsupported HTTP method. In such cases, the reason will be reported to your application by notification.
Let's see how your code asks Microvisor to submit an HTTP request on its behalf. Having opened a channel of type MV_CHANNELTYPE_HTTP
, you use the System Call mvSendHttpRequest()
to make an HTTP request through that channel. This function takes the host channel's handle and a pointer to a request configuration structure — an MvHttpRequest
record — as arguments.
The MvHttpRequest
record specifies the request that you would like to be sent. It looks like this:
1struct MvHttpRequest {2struct MvSizedString method;3struct MvSizedString url;4uint32_t num_headers;5const struct MvHttpHeader *headers;6struct MvSizedString body;7uint32_t timeout_ms;8};
The values of method
, url
and body
are data structures holding, respectively, the HTTP method; the target resource's protocol, full domain name and path; and any body data. The structures combine the values as byte data and the number of bytes each value comprises. For example:
1const char verb[] = "GET";2const char uri[] = "https://jsonplaceholder.typicode.com/todos/1";3const char body[] = "";45struct MvHttpRequest request_config = {6.method = {7.data = (uint8_t *)verb,8.length = strlen(verb)9},10.url = {11.data = (uint8_t *)uri,12.length = strlen(uri)13},14...15.body = {16.data = (uint8_t *)body,17.length = strlen(body)18},19...20};
Microvisor supports only these methods: GET
, POST
, PUT
, PATCH
, HEAD
, DELETE
, and OPTIONS
. If you specify any other method it will be rejected. This does not take place when you call mvSendHttpRequest()
but when the request is processed by the Microvisor cloud, and so you will receive the error via a notification.
At this time, Microvisor supports only HTTPS, indicated by the prefix https://
in your url
value. All other protocols/prefixes will be rejected, reported by notification.
Request headers are added to the record as an array of 8-byte MvHttpHeader
structures. Each of these comprises a pointer to byte data holding the header text, and the number of bytes in the header. For example:
1const char header_text[] = "Content-Type: application/json";2struct MvHttpHeader header = {3.data = (uint8_t *)header_text,4.length = strlen(header_text)5};67MvHttpHeader headers[] = { header };89struct MvHttpRequest request_config = {10...,11.headers = headers,12.num_headers = 1,13...14};
Microvisor automatically adds a Host:
header for you. It also includes a Content-Length:
header based on the size of any data you include. You do not need to add this yourself, but if you do, it will be ignored.
Lastly, the MvHttpRequest
's timeout
value is an integer in the range 5000 to 10000 — the timeout period in milliseconds:
1struct MvHttpRequest request_config = {2...,3.timeout_ms = 100004};
To issue the request using this configuration, make the System Call, mvSendHttpRequest()
:
1// Issue the request -- and check its status2enum MvStatus status = mvSendHttpRequest(http_channel_handle,3&request_config);4if (status == MV_STATUS_OKAY) {5server_log("Request accepted and relayed to the cloud");6} else {7server_error("Microvisor rejected request with error: %i", status);8}
The variable http_channel_handle
is of type MvChannelHandle
. Its address is included in the channel configuration record used to open the channel. Microvisor writes the channel's handle to that address when the channel is open.
The functions server_log()
and server_error()
are functions which take a format string and zero or more value arguments, render the string, and issue it via mvServerLog
. You can view both functions' code in the HTTP demo repo.
Requests that are accepted by Microvisor will be processed asynchronously. All responses will be signaled with an MV_EVENT_CHANNELDATAREADABLE
notification. When your application receives such a notification from an HTTP channel — remember, you establish a notification center for this purpose when you configure the channel — it should call the System Call function mvReadHttpResponseData()
to determine the state of the issued request.
Microvisor disallows System Calls to be issued from with interrupt service routines (ISRs). When your specified channel notification ISR is triggered, either set a flag that can be read by your main code loop and used to make the mvReadHttpResponseData()
call, or make use of FreeRTOS' inter-task queues and xQueueSendFromISR()
function.
For example, if the TIM8
interrupt is being used for notifications:
1void TIM8_BRK_IRQHandler(void) {2// Get the event type3enum MvEventType event_kind = http_notification_center->event_type;45if (event_kind == MV_EVENTTYPE_CHANNELDATAREADABLE) {6// Flag we need to access received data and to close the HTTP channel7// when we're back in the main loop. This lets us exit the ISR quickly.8// We should not make Microvisor System Calls in the ISR.9request_recv = true;10}11}
mvReadHttpResponseData()
takes a handle that indicates the HTTP channel to probe — most likely the same one used to host the request you issued. It also takes a pointer to 16 bytes of memory into which Microvisor will write an MvHttpResponseData
record. This contains information on the request's status:
1struct MvHttpResponseData {2enum MvHttpResult result;3uint32_t status_code;4uint32_t num_headers;5uint32_t body_len;6};
The key property is result
, and this should be checked first. It indicates the status of the request: did it succeed, or did it fail? And if it failed, why did it do so? Was it a configuration error, or was the request rejected by the target server? The value of result will indicate a possible cause, all of which are shown on the table below.
Constant | Description |
---|---|
MV_HTTPRESULT_OK | The HTTP request was sent — but check the status code |
MV_HTTPRESULT_UNSUPPORTEDURISCHEME | The HTTP request was not sent because of an unsupported URI scheme, e.g., http:// or ftp:// , was used |
MV_HTTPRESULT_UNSUPPORTEDMETHOD | The HTTP request was not sent because an unsupported HTTP method was used in the request |
MV_HTTPRESULT_INVALIDTIMEOUT | The HTTP request was not sent because an invalid timeout was specified in the request. It should be in the range 5000-10000 |
MV_HTTPRESULT_INVALIDHEADERS | The HTTP request was not sent because invalid headers were provided in the request |
MV_HTTPRESULT_REQUESTFAILED | The HTTP request was not sent for some other reason, such as a timeout or a DNS lookup failure |
MV_HTTPRESULT_RESPONSETOOLARGE | The HTTP request was sent, but the HTTP response returned by the server didn't fit into the HTTP channel's receive buffer. Increase the size of your RX buffer |
Even a successful request send (result
is MV_HTTPRESULT_OK
) may not result in a successful response, so it's essential to follow this up by checking the value of the MvHttpResponseData
record's status_code
value, which is the standard HTTP response code for the transaction.
Here's a typical function of the kind you might use to read an MvHttpResponseData
record:
1void http_process_response(void) {2// We have received data via the active HTTP channel so establish3// an `MvHttpResponseData` record to hold response metadata4static struct MvHttpResponseData resp_data;5enum MvStatus status = mvReadHttpResponseData(http_channel_handle,6&resp_data);7if (status == MV_STATUS_OKAY) {8// Check we successfully issued the request (`result` is OK) and9// the request was successful (status code 200)10if (resp_data.result == MV_HTTPRESULT_OK) {11if (resp_data.status_code == 200) {12// Set up a buffer that we'll get Microvisor to write the response body into13uint8_t buffer[resp_data.body_len + 1];14memset((void *)buffer, 0x00, resp_data.body_length + 1);15status = mvReadHttpResponseBody(http_channel_handle,160,17buffer,18resp_data.body_length);19if (status == MV_STATUS_OKAY) {20// Retrieved the body data successfully so log it21server_log("%s", buffer);22} else {23server_error("HTTP response body read status %i", status);24}25} else {26server_error("HTTP status code: %lu", resp_data.status_code);27}28} else {29server_error("Request failed. Status: %lu", (uint32_t)resp_data.result);;30}31} else {32server_error("Response data read failed. Status: %i", status);33}34}
Assuming your request was successfully sent by the Microvisor cloud (result
is MV_HTTPRESULT_OK
) and you received the expected status code (for example, 200), then your code can access the response from the server.
The response's body data size and header count are indicated by the MvHttpResponseData
record's remaining fields. Use the two System Calls mvReadHttpResponseHeader()
and/or mvReadHttpResponseBody()
to access all or a portion of these sources. You can make these calls from within your notification interrupt handler, or set flags so that they can be called from within your main program loop.
Because such flag variables may be modified within the interrupt handler, it's important to declare them using the C keyword volatile
. For example:
volatile bool request_received = false;
This ensures the compiler doesn't apply optimizations which may render the variable unwriteable by the interrupt handler code.
To access the response's body data, call mvReadHttpResponseBody()
. It takes the handle of the channel that passed the source request, a byte offset from the start of the data from which to begin reading, a pointer to memory into which the body data will be written by Microvisor, and the size of the buffer in bytes.
Specify an offset of zero to read the data from the start. Microvisor will write as many bytes as it can until it hits either the end of the data, or the end of the buffer, whichever comes first.
The following code, taken from the previous example, reads body data from the start (offset
= 0) into a suitably-sized and null-terminated buffer which is then output.
1uint8_t buffer[resp_data.body_length + 1];2memset((void *)buffer, 0x00, resp_data.body_length + 1);3status = mvReadHttpResponseBody(http_channel_handle,40,5buffer,6resp_data.body_length);7if (status == MV_STATUS_OKAY) {8// Retrieved the body data successfully so log it9server_log("%s", buffer);10}
Response headers are stored as an array of MvHttpHeader
records, which we discussed earlier. Pass mvReadHttpResponseHeader()
the index of the header you want to read. For example, if there are three headers (the MvHttpResponseData
record's num_headers
value is 3), you can access each in turn by iterating over indices 0 through 2.
The call has four parameters: the handle of the HTTP channel that passed the source request, the index of the header you require, a pointer to the memory into which the header will be written by Microvisor, and the size of that buffer in bytes. Again, Microvisor will write as many bytes as it can until it hits either the end of the header text, or the end of the write-buffer, whichever comes first.
1void output_headers(uint32_t n) {2enum MvStatus status = MV_STATUS_OKAY;3uint8_t buffer[81];4for (uint32_t i = 0 ; i < n ; i++) {5memset((void *)buffer, 0x00, 81);6status = mvReadHttpResponseHeader(http_handles.channel, i, buffer, 80);7if (status == MV_STATUS_OKAY) {8server_log("%lu. %s", i + 1, buffer);9}10}11}
Now that you have the data you requested — or at least an indication as to why it was not available — you code must close the HTTP channel it used. As noted earlier, this channel can't be used for any further requests, even duplicates of the one you just issued. Close the channel in the usual way. Depending on your use case, you may also wish to release the host network, but you might also choose to keep this up and ready for the next HTTP request your code needs to make.
Whatever course of action you take at this point in your code, it is important not to close the channel (or release the network) within the notification interrupt handler. Typically, you will set a flag which your main code loop can check and, if the flag is set, safely close the channel.
1void http_close_channel(void) {2// If we have a valid channel handle -- ie. it is non-zero --3// then ask Microvisor to close it and confirm acceptance of4// the closure request.5if (http_channel_handle != 0) {6enum MvStatus status = mvCloseChannel(&http_channel_handle);7server_log("HTTP channel closed");8assert(status == MV_STATUS_OKAY);9}1011// Confirm the channel handle has been invalidated by Microvisor12assert(http_channel_handle == 0);13}
The code used in this guide is available in full as a buildable application.