Skip to main content

Networking

The Hosanna UI Framework provides a robust networking layer that enables cross-platform HTTP communication. This documentation covers the key components and usage patterns for making network requests in your Hosanna applications.

Live Example

Ui And Interaction Networking

@view('UiAndInteractionNetworking')
export class UiAndInteractionNetworkingView extends BaseExampleScreenView<UiAndInteractionNetworkingState> {

  @state loading = '';
  @state result = '';

  protected getViews(): ViewStruct<ViewState>[] {

    // --- edit below ---

    return [
      VGroup([
        Button({ text: 'Fetch Posts' })
          .isInitialFocus(true)
          .onClick(() => this.fetchExample()),
        Label({ text: `State: ${this.loading}` }),
        Label({ text: this.result, width: 1000, wrap: true }),
      ]).itemSpacing(16)
        .translation([50, 50])
    ]

    // --- edit above ---
  }

  private fetchExample() {
    this.loading = 'loading';
    this.result = '';
    const url = 'https://jsonplaceholder.typicode.com/posts/1';
    return this.dispatch<IHsFetchResponse>(HttpMethod.Get, { url })
      .then((response) => {
        this.loading = 'loaded';
        this.result = JSON.stringify(response.json);
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .catch((reason: any) => {
        this.loading = 'error';
        this.result = String(reason);
      });
  }
}
Try It

Trigger a request and observe how the view updates from the async response.

HsFetch

The core of Hosanna's networking capabilities is the HsFetch class, which provides a platform-agnostic implementation of the Fetch API. It works across Roku, Apple TV, Android TV, Samsung TV, iOS, Android, and Web through the active platform adapter.

Basic Usage

const hosannaFetch = new HsFetch();
const response = await hosannaFetch.fetch(url, options);

Supported HTTP Methods

The framework supports all standard HTTP methods through the HttpMethod enum:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD

HTTP Commands

Hosanna provides a high-level abstraction for HTTP requests through the HttpCommands class. This integrates with Hosanna's async command system for better state management and testing capabilities.

Making HTTP Requests

In a Hosanna view, you can make HTTP requests using the dispatch method:

// GET request
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data'
})
.then((response) => {
// Handle response
})
.catch((error) => {
// Handle error
});

// POST request with body
this.dispatch<IHsFetchResponse>(Http.Post, {
url: 'https://api.example.com/data',
options: {
body: JSON.stringify({ key: 'value' }),
headers: {
'Content-Type': 'application/json'
}
}
});

Post-Processing Responses

You can automatically process HTTP responses using the postProcessFunction option. This function is invoked automatically after a successful HTTP response, allowing you to update UI state, show notifications, or cache data without promise chains.

Tip: Cleaner Code

Use postProcessFunction when you want automatic post-processing without promise chains. It's especially useful for updating UI state immediately after successful responses.

AsyncFunctionPointer Under the Hood

On platforms that use background tasks (like Roku), HsFetchOptions.postProcessFunction and transformFunction are typed as AsyncFunctionPointer. You must pass exported functions (no inline callbacks or .bind(this)) so Hosanna can serialize the function pointer and invoke it safely from a Roku Task or other background thread.

Basic Usage

Define a post-processing function in a separate module and export it, then pass it to your HTTP command. The function always receives a single IHsFetchResponse parameter:

// apiHandlers.ts
export function handleApiResponse(response: IHsFetchResponse) {
console.info('API call completed:', response);
}

// In your view class method
import { handleApiResponse } from './apiHandlers';

// Use with HTTP command
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/users',
headers: { 'Authorization': 'Bearer token' },
// Exported function reference; receives IHsFetchResponse only
postProcessFunction: handleApiResponse
});

Inline Functions Will Be Rejected

Inline arrow functions are convenient, but they are not supported where AsyncFunctionPointer is used (such as HsFetchOptions.postProcessFunction and transformFunction). The linter will report a diagnostic if you try this pattern:

// Incorrect: this looks convenient but will be rejected
this.dispatch<IHsFetchResponse>(Http.Post, {
url: 'https://api.example.com/submit',
body: formData,
postProcessFunction: (result: IHsFetchResponse) => {
if (result.ok) {
this.showSuccessMessage();
} else {
this.showErrorMessage(result.statusText);
}
}
});
Use Exported Functions Instead

Export the function (or a factory that returns a closure) at module scope and pass that exported function to postProcessFunction or transformFunction. Hosanna's linter flags inline or .bind(this) callbacks in these slots so they can be safely executed in a Roku Task or other background thread.

With Promise Chains

Traditional promise handling still works and can be combined with postProcessFunction:

export function cacheResponse(response: IHsFetchResponse) {
console.info('Automatic post-processing', response.status);
}

this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data',
postProcessFunction: cacheResponse
})
.then((response: IHsFetchResponse) => {
// Traditional promise handling still works
console.info('Response:', response);
});
Key Props
  • postProcessFunction: Optional function that receives IHsFetchResponse as its first argument
  • Automatic invocation: Called automatically after successful HTTP responses
  • Error isolation: Post-processing errors are logged but don't fail the HTTP request
  • Works with mocks: Post-processing also works with mock responses in development

Use Cases

Critical: Must Be Exported Functions

AsyncFunctionPointer cannot be inline or anonymous functions — they must be exported functions defined at module scope.

Update UI state after data fetch:

// Correct: export the function first
export function updateUserProfile(response: IHsFetchResponse) {
// Update your app's state using services or stores that are safe to access
console.info('Profile loaded', response.json);
}

this.dispatch<IHsFetchResponse>(Http.Get, {
url: '/api/user/profile',
postProcessFunction: updateUserProfile // Receives IHsFetchResponse only
});

Show notifications:

// Correct: export the function first
export function handleSubmitResponse(response: IHsFetchResponse) {
if (response.ok) {
console.info('Success!');
}
}

this.dispatch<IHsFetchResponse>(Http.Post, {
url: '/api/submit',
body: formData,
postProcessFunction: handleSubmitResponse
});

Cache responses:

// Correct: export the function (no instance context needed)
export function cacheResponse(response: IHsFetchResponse) {
localStorage.setItem('cachedData', JSON.stringify(response.json));
}

this.dispatch<IHsFetchResponse>(Http.Get, {
url: '/api/data',
postProcessFunction: cacheResponse // Use exported function directly
});
Important Constraints
  • Must be exported functions: AsyncFunctionPointer cannot be inline or anonymous functions — they must be exported functions defined at module scope.
  • Parameter limit: Functions support up to 5 parameters maximum
  • Module scope: Functions must be accessible from module scope (not nested functions, closures, or inline arrow functions)
  • Serialization: Functions are serialized as string pointers, so closures and this context are not captured directly
  • Resolution: Functions must be resolved using hs_resolveAsyncFunctionPointer() before calling (handled automatically by the framework)

Passing function pointers across tasks

You can pass a function pointer to be executed in a background task using AsyncFunctionPointer. Functions are automatically serialized as string pointers and resolved when invoked, allowing safe execution across async boundaries.

import { processRows } from './transformFunctions';

this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data',
transformFunction: processRows // Function reference, not object
});
How It Works

The function is serialized as an AsyncFunctionPointer (string pointer) when dispatched. The runtime automatically resolves it using hs_resolveAsyncFunctionPointer() before execution in the background task.

Type signature:

// hosanna-api
declare type AsyncFunctionPointer = (...args: any[]) => any;
declare function hs_resolveAsyncFunctionPointer(ptr: AsyncFunctionPointer): Function;
Constraints
  • Must be exported functions: AsyncFunctionPointer cannot be inline or anonymous functions — they must be exported functions defined at module scope.
  • Functions support up to 5 parameters maximum
  • Functions must be accessible from module scope (not nested functions, closures, or inline arrow functions)
  • Functions are serialized as string pointers, so closures and this context are not captured directly

Features

1. Cancellation Support

Requests can be cancelled using the HsCancellationToken:

const cancellationToken = new HsCancellationToken();
hosannaFetch.fetch(url, options, cancellationToken);

// Cancel the request when needed
cancellationToken.cancel();

2. Mock Responses

For testing and development purposes, Hosanna supports mock responses through the developer utilities:

interface IDeveloperUtils {
getMockResponseForHttpRequest(method: HttpMethod, url: string): IHsFetchResponse | undefined;
}

3. Response Handling

The IHsFetchResponse interface provides a standardized way to handle responses:

interface IHsFetchResponse {
ok: boolean;
json: any;
text: string;
status: number;
statusText: string;
headers: Record<string, string>;
ipAddress: string;
}
Response Properties
  • ok: true if status is 200-299, false otherwise
  • json: Parsed JSON if Content-Type is application/json
  • text: Raw text response if not JSON
  • status: HTTP status code
  • statusText: HTTP status text
  • headers: Response headers as key-value pairs
  • ipAddress: Target IP address of the request

Best Practices

  1. Error Handling: Always implement proper error handling for network requests:

    this.dispatch<IHsFetchResponse>(Http.Get, { url })
    .then((response) => {
    if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json;
    })
    .catch((error) => {
    console.error('Network request failed:', error);
    });
  2. Request Cancellation: Use cancellation tokens for long-running requests that might need to be cancelled.

  3. Headers: Set appropriate headers for your requests, especially for content types and authentication:

    const options = {
    headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your-token'
    }
    };
  4. Response Type Handling: Use TypeScript types to ensure type safety when handling responses:

    interface MyDataType {
    id: number;
    name: string;
    }

    this.dispatch<IHsFetchResponse>(Http.Get, { url })
    .then((response) => response.json as MyDataType)
    .then((data) => {
    // TypeScript knows the shape of 'data'
    });
  5. Post-Processing Functions: Use postProcessFunction for automatic response handling when you want cleaner, more declarative code. Use promise chains when you need complex error handling or multiple async operations:

    export function updateUiFromResponse(response: IHsFetchResponse) {
    console.info('Received data', response.json);
    }

    // Use postProcessFunction for simple, automatic processing
    this.dispatch<IHsFetchResponse>(Http.Get, {
    url: '/api/data',
    postProcessFunction: updateUiFromResponse
    });

    // Use promise chains for complex logic
    this.dispatch<IHsFetchResponse>(Http.Get, { url })
    .then((response) => {
    if (!response.ok) throw new Error('Failed');
    return processData(response.json);
    })
    .then((data) => this.updateUI(data))
    .catch((error) => this.handleError(error));
Tip: Error Isolation

Post-processing function errors are logged but don't fail the HTTP request. This ensures your network operations succeed even if post-processing encounters issues.

Platform-Specific Considerations

The networking layer automatically handles platform-specific implementations:

  • Web: Uses the native Fetch API
  • Roku: Uses roUrlTransfer under the hood
  • Apple TV, Android TV, iOS, and Android: Use native platform networking through the platform target
  • Samsung TV: Uses the web-based TV target networking path

Example Implementation

Here's a complete example of implementing a data fetching view:

@view('DataFetchingView')
export class DataFetchingView extends BaseView<ViewState> {
private loadData() {
const url = 'https://api.example.com/data';

return this.dispatch<IHsFetchResponse>(Http.Get, {
url,
options: {
headers: {
'Content-Type': 'application/json'
}
}
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json;
})
.catch((error) => {
console.error('Failed to fetch data:', error);
// Handle error in UI
});
}
}