libwebsockets文件下载缺失Content-Disposition头的解决方案


阅读 2 次

问题现象描述

在使用libwebsockets实现HTTPS文件服务时,发现响应头中缺少Content-Disposition: attachment字段,导致浏览器无法自动触发下载行为。以下是问题复现的关键代码片段:

struct lws_http_mount mount = {
    .mount_next = NULL,
    .mountpoint = "/",
    .origin = "var/www/",
    .extra_mimetypes = "application/zip",
    .origin_protocol = LWSMPRO_FILE
};

if (lws_serve_http_file(wsi, "/tmp/log.zip", "application/zip", NULL, 0) < 0) {
    lwsl_err("Failed to serve file: %s\n", "/tmp/log.zp");
    lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL);
    return;
}

核心问题分析

libwebsockets的默认文件服务实现不会自动添加Content-Disposition头,这是设计上的选择。我们需要通过回调函数手动添加这个关键响应头。

完整解决方案

以下是实现文件下载功能的完整示例代码,包含自定义响应头的处理:

static int callback_http(
    struct lws *wsi,
    enum lws_callback_reasons reason,
    void *user, void *in, size_t len)
{
    switch (reason) {
        case LWS_CALLBACK_HTTP: {
            // 检查请求路径是否为下载文件
            if (strcmp((const char *)in, "/download") == 0) {
                // 设置响应头
                unsigned char buffer[1024];
                unsigned char *start = buffer, *end = &buffer[sizeof(buffer)-1];
                
                // 添加Content-Disposition头
                if (lws_add_http_header_by_name(wsi,
                    (unsigned char *)"content-disposition",
                    (unsigned char *)"attachment; filename=\"logs.zip\"",
                    32, &start, end)) {
                    return -1;
                }
                
                // 设置MIME类型
                if (lws_add_http_header_content_type(wsi,
                    "application/zip", &start, end)) {
                    return -1;
                }
                
                // 结束头处理
                if (lws_finalize_http_header(wsi, &start, end)) {
                    return -1;
                }
                
                // 开始写入响应体
                lws_callback_on_writable(wsi);
                return 0;
            }
            break;
        }
        
        case LWS_CALLBACK_HTTP_FILE_COMPLETION:
            // 文件传输完成后的处理
            return -1;
            
        case LWS_CALLBACK_HTTP_WRITEABLE: {
            // 实际发送文件内容
            char buf[4096];
            int fd = open("/tmp/log.zip", O_RDONLY);
            if (fd < 0) return -1;
            
            int n = read(fd, buf, sizeof(buf));
            if (n < 0) {
                close(fd);
                return -1;
            }
            
            if (lws_write(wsi, (unsigned char *)buf, n, LWS_WRITE_HTTP) != n) {
                close(fd);
                return -1;
            }
            
            close(fd);
            break;
        }
    }
    return 0;
}

替代方案:使用lws_serve_http_file的扩展

如果你更倾向于使用lws_serve_http_file的简化接口,可以通过以下方式扩展功能:

int serve_file_with_disposition(struct lws *wsi, const char *filepath, 
                              const char *mimetype, const char *disposition) {
    // 先设置响应头
    unsigned char buffer[1024];
    unsigned char *start = buffer, *end = &buffer[sizeof(buffer)-1];
    
    if (lws_add_http_header_by_name(wsi,
        (unsigned char *)"content-disposition",
        (unsigned char *)disposition,
        strlen(disposition), &start, end)) {
        return -1;
    }
    
    // 然后调用标准文件服务
    return lws_serve_http_file(wsi, filepath, mimetype, NULL, 0);
}

// 使用示例
serve_file_with_disposition(wsi, "/tmp/log.zip", 
                          "application/zip", 
                          "attachment; filename=\"logs.zip\"");

测试验证

使用Postman或curl测试时,现在应该能看到完整的响应头:

HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="logs.zip"
Content-Length: 123456
Connection: keep-alive

常见问题排查

  • 确保回调函数已正确注册到协议中
  • 检查文件路径是否有读取权限
  • 验证MIME类型设置是否正确
  • 如果使用HTTPS,确保证书配置正确