Menu

Expand
Rate this page:

How to Issue HTTP Requests under Microvisor

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 channels

HTTP communications involves using the specific channel type MV_CHANNEL_TYPE_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:

// Set up the HTTP channel's send and receive buffers
static volatile uint8_t http_channel_rx_buffer[1536] __attribute__((aligned(512)));
static 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:

  1. Your code submits the request to Microvisor which accepts or rejects it. We’ll explore this in the next section.
  2. An accepted request is relayed by Microvisor to the Twilio Cloud and then transmitted to the server referenced in the request record.

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.

HTTP requests

Let’s see how your code asks Microvisor to submit an HTTP request on its behalf. Having opened a channel of type MV_CHANNEL_TYPE_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.

Request Configuration

The MvHttpRequest record specifies the request that you would like to be sent. It looks like this:

struct MvHttpRequest {
  uint8_t *method;
  uint32_t method_length;
  uint8_t *url;
  uint32_t url_length;
  uint32_t num_headers;
  const struct MvHttpHeader *headers;
  uint8_t *body;
  uint32_t body_length;
  uint32_t timeout;
};

The values of method, url and body are pointers to byte data holding, respectively, the HTTP method; the target resource’s protocol, full domain name and path; and finally any body data. The respective _length values indicate the number of bytes in each of those values. For example:

const char verb[] = "GET";
const char uri[] = "https://jsonplaceholder.typicode.com/todos/1";
const char body[] = "";

struct MvHttpRequest request_config = {
  .method = (uint8_t *)verb,
  .method_len = strlen(verb),
  .url = (uint8_t *)uri,
  .url_len = strlen(uri),
  ...,
  .body = (uint8_t *)body,
  .body_len = strlen(body),
  ...
};

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 Twilio 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:

const char header_text[] = "Content-Type: application/json";
struct MvHttpHeader header = {
  .data = (uint8_t *)header_text,
  .length = strlen(header_text)
};

MvHttpHeader headers[] = { header }; 

struct MvHttpRequest request_config = {
  ...,
  .headers = headers,
  .num_headers = 1,
  ...
};

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:

struct MvHttpRequest request_config = {
    ...,
    .timeout_ms = 10000
};

The full structure is therefore:

struct MvHttpRequest request_config = {
    .method = (uint8_t *)verb,
    .method_len = strlen(verb),
    .url = (uint8_t *)uri,
    .url_len = strlen(uri),
    .num_headers = 1,
    .headers = headers,
    .body = (uint8_t *)body,
    .body_len = strlen(body),
    .timeout_ms = 10000
};

To issue the request using this configuration, make the System Call:

// Issue the request -- and check its status
enum MvStatus status = mvSendHttpRequest(http_channel_handle,
                                         &request_config);
if (status == MV_STATUS_OKAY) {
  printf("[DEBUG] Request accepted and relayed to Twilio\n");
} else {
  printf("[ERROR] Microvisor rejected request with error: %i\n", status);
}

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.

Managing request outcomes

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:

void TIM8_BRK_IRQHandler(void) {
  // Get the event type
  enum MvEventType event_kind = http_notification_center->event_type;

  if (event_kind == MV_EVENTTYPE_CHANNELDATAREADABLE) {
    // Flag we need to access received data and to close the HTTP channel
    // when we're back in the main loop. This lets us exit the ISR quickly.
    // We should not make Microvisor System Calls in the ISR.
    request_recv = true;
  }
}

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:

struct MvHttpResponseData {
  enum MvHttpResult result;
  uint32_t status_code;
  uint32_t num_headers;
  uint32_t body_len;
};

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.

Example

Here’s a typical function of the kind you might use to read an MvHttpResponseData record:

void http_process_response(void) {
  // We have received data via the active HTTP channel so establish
  // an `MvHttpResponseData` record to hold response metadata
  static struct MvHttpResponseData resp_data;
  enum MvStatus status = mvReadHttpResponseData(http_channel_handle, 
                                                &resp_data);
  if (status == MV_STATUS_OKAY) {
    // Check we successfully issued the request (`result` is OK) and
    // the request was successful (status code 200)
    if (resp_data.result == MV_HTTPRESULT_OK) {
      if (resp_data.status_code == 200) {
        // Set up a buffer that we'll get Microvisor to write the response body into
        uint8_t buffer[resp_data.body_length + 1];
        memset((void *)buffer, 0x00, resp_data.body_length + 1);
        status = mvReadHttpResponseBody(http_channel_handle, 
                                                0, 
                                        buffer, 
                                        resp_data.body_length);
        if (status == MV_STATUS_OKAY) {
          // Retrieved the body data successfully so log it
          printf("%s\n", buffer);
        } else {
          printf("[ERROR] HTTP response body read status %i\n", status);
        }
      } else {
        printf("[ERROR] HTTP status code: %lu\n", resp_data.status_code);
      }
    } else {
      printf("[ERROR] Request failed. Status: %lu\n", (uint32_t)resp_data.result);;
    }
  } else {
    printf("[ERROR] Response data read failed. Status: %i\n", status);
  }
}

Reading responses

Assuming your request was successfully sent by the Twilio 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.

Response data

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.

Example

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 using printf().

uint8_t buffer[resp_data.body_length + 1];
memset((void *)buffer, 0x00, resp_data.body_length + 1);
status = mvReadHttpResponseBody(http_channel_handle, 
                                0, 
                                buffer, 
                                resp_data.body_length);
if (status == MV_STATUS_OKAY) {
  // Retrieved the body data successfully so log it
  printf("%s\n", buffer);
}

Response headers

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.

Example

void output_headers(uint32_t n) {
  enum MvStatus status = MV_STATUS_OKAY;
  uint8_t buffer[81];
  for (uint32_t i = 0 ; i < n ; i++) {
    memset((void *)buffer, 0x00, 81);
    status = mvReadHttpResponseHeader(http_handles.channel, i, buffer, 80);
    if (status == MV_STATUS_OKAY) {
      printf("%lu. %s\n", i + 1, buffer);
    }
  }
}

Cleaning up

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.

void http_close_channel(void) {
  // If we have a valid channel handle -- ie. it is non-zero --
  // then ask Microvisor to close it and confirm acceptance of
  // the closure request.
  if (http_channel_handle != 0) {
    enum MvStatus status = mvCloseChannel(&http_channel_handle);
    printf("[DEBUG] HTTP channel closed\n");
    assert(status == MV_STATUS_OKAY);
  }

  // Confirm the channel handle has been invalidated by Microvisor
  assert(http_channel_handle == 0);
}

The code used in this guide is available in full as a buildable application.

Rate this page:

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd by visiting Twilio's Stack Overflow Collective or browsing the Twilio tag on Stack Overflow.

Thank you for your feedback!

Please select the reason(s) for your feedback. The additional information you provide helps us improve our documentation:

Sending your feedback...
🎉 Thank you for your feedback!
Something went wrong. Please try again.

Thanks for your feedback!

thanks-feedback-gif