Serving favicon for Dash applications on AWS Lambda + API Gateway

While using AWS Lambda + AWS API Gateway for hosting and accessing my Dash applications, I had a lot of issues getting the favicon to load for my apps. After extensive trial and error, I finally found a solution and wanted to share it here, since solutions to this problem are not available on the web so far.

Problem

Favicon data is not loaded in the browser because API Gateway blocks the binary data of the favicon.ico file.

Context

  • This solution was tested with Dash 2.3.1
  • The application was deployed to AWS lambda and web access was arranged using AWS API Gateway
  • The API type used was a REST API
  • Server responses were handled using the aws-wsgi library.
  • AWS applications were build and deployed using AWS SAM CLI, i.e. sam build --use-container and sam deploy command-line commands.

Solution

The core issue is that binary data is blocked by the REST API by default. The problem is solved by 1. allowing the API to serve the correct binary types and 2. making sure that favicon is decoded as binary data.

Setting up API and Lambda resources

To deploy to AWS cloud with SAM CLI, you have to create a template.yaml where you configure your AWS resources. Below is an excerpt of the template that I used in my application (fyi: below is not the complete template).

Resources:
  DashApi:
    Type: AWS::Serverless::Api
    Properties:
      BinaryMediaTypes:
        - "image/*"
  
  MyLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
        Events:
          DashEndpoint:
            Type: Api
            Properties:
              Path: /{proxy+}
              Method: ANY
              RestApiId:
                Ref: DashApi

The important thing here is to set BinaryMediaTypes because this will tell your API which data to pass as binary data and which to pass as utf-8 encoded data. In this example I set all image types as binary. It is also possible to set all content types as binary with "*/*", or list specific image types only.

Handling the response with aws-wsgi

To access point to your application on the AWS Lambda side is done using a lambda_handler function. A very straightforward way to do that is to use awsgi.response(app.server, event, context, base64_content_types) from the aws-wsgi library.
However, the call in this library will decode everything as utf-8 by default, unless it’s told otherwise specifically (see the topic I created previously). For this you have to set the base64_content_types input parameter (list of strings). Below is a very simple implementation of a lambda_handler that is enough to make your application running.

from your.dash.application.app import app

def lambda_handler(event, context):
    base64_content_types = ['image/vnd.microsoft.icon', 'image/x-icon']
    return awsgi.response(app.server, event, context, base64_content_types )

Note: the asterix wildcard doesn’t work in base64_content_types since awsgi does a literal comparison of Content-Type with all entries in the list. So you have to list all desired base 64 content types explicitly.

Note 2: Icons are by default either image/vnd.microsoft.icon or image/x-icon MIME types. Even though Dash sets the HTML header for favicon to x-icon doesn’t mean that awsgi.response() perceives it as that type. If your favicon is of type png be sure to include image/png to the list.

Conclusion

By correctly setting the above to parts, I was able to load favicon correctly. While this solution works for my particular context, it doesn’t mean that it cannot be ported to other ways of hosting Dash applications in AWS. If you have such examples feel free to share them below and letting us know how you adapted the above to make favicon work in your cloud context. I hope this information will save a lot of headaches and time for you.

2 Likes

Thank you for sharing your trails and errors and solutions with us, @Tobs

Hi @Tobs - thanks for sharing your process, this is helpful to me. Can you also share a link to your github repository, or any additional documentation about how you deployed your Dash app with AWS Lambda and Gateway? The only tutorial I’ve found is the following: GitHub - ratajczak/dash-lambda: Example of a Dash application deployed on AWS Lambda

I did also find a couple of related issues:

Thanks for any info you can share about how you did this.

Sorry for the delayed reply @austin. Unfortunately, I cannot share a github link with some example code. What issues are you running into? I can guide you in the process.

Step 1: install AWS SAM CLI. This allows you to deploy your tools from the terminal (in Pycharm for example)

Step 2: In your dash application folder, create a template.yaml file that will contain the configurations of your lambda.
Usually I configure my folder as such:

- top_dir
|- support_lib_dir 
|- dash_app_dir
|- samconfig.toml
|- template.yaml

Then, your template.yaml can be something like:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Sample SAM cd Template for Dash

Parameters: 
  ServiceName:
    Type: String
    Default: "<my_service_name>"
  BasePathName:
    Type: String
    Default: <my_base_path>
    Description: Specific app reference that gets added to the base url for your application

Globals:
  Function:
    Runtime: python3.8
    MemorySize: 3072
    Timeout: 600
    Tags:
      Project: "<my_project_name>"
      Name: "<my_application_name>"
      Service: !Ref ServiceName

Resources:
  DashApi:
    Type: AWS::Serverless::Api
    Properties:
      BinaryMediaTypes:
        - "image/*"
      StageName: !Ref BasePathName
  
  <MyLambdaFunction>:
    Type: AWS::Serverless::Function
    Properties:
      Handler: <dash_app_dir>.<lambda_handler_file.py>.lambda_handler
      Runtime: Python 3.8
      FunctionName: "<my_cloudwatch_log_group_name>" 
      MemorySize: 2048
      Timeout: 900
      Events:
        DashEndpoint:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY
            RestApiId:
              Ref: DashApi

  BasePathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      BasePath: !Ref BasePathName
      DomainName: "<my_base_domain: e.g. my_website.com>"
      Stage: !Ref BasePathName
      RestApiId:
        Ref: DashApi
    DependsOn:
      - DashApiStage

Outputs:
  DashAppURL:
    Value: "<my_external_url_that_I_share_with_people. Usually I do "my_base_domain/BasePathName/dash">"

Your template can be more elaborate of course, but this example showcases some more advanced functionality you can use (especially if you are new to AWS). Just replace all the <...> parts by your own names.

samconfig.toml can be something like:

version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "<my_cloudformation_stack_name>"
region = "<my_region>"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"

Step 3: Build the application

sam build --use-container --manifest ./dash_app_dir/requirements.txt

Step 4: Deploy the application

sam deploy --no-confirm-changeset

Or just

sam deploy

if you do want to confirm the changes (since this was set as default in samconfig.toml)

EDIT: if you set the BasePathName like above in the template, you also need to set that in the server configuration, i.e. when creating your app object. This is how I initialize my app:

app = Dash(__name__,
		   serve_locally=False,
		   external_stylesheets=[dbc.themes.BOOTSTRAP],
		   assets_folder=os.path.join(location, 'assets/'),
		   compress=False, routes_pathname_prefix='/<my_base_path_name>/',
		   requests_pathname_prefix='/<my_base_path_name>/')

This should get you started. If you have specific issues, you can post them here.