Giraffe需手动解析multipart/form-data中的XML文件:先用ctx.Request.ReadFormAsync()获取IFormFile,再用XmlReader流式安全解析,禁用DTD、限制大小,并用Result类型返回解析结果。
Giraffe 本身不直接处理文件上传,XML 文件上传需依托 ASP.NET Core 的 IFormFile 基础能力 + F# 惯用解析逻辑,关键在“接收”和“解析”两步解耦,而非框架内置支持。
ASP.NET Core 的模型绑定机制默认不自动解析 multipart/form-data 中的文件为 IFormFile,必须显式启用并配置。Giraffe 的 HttpHandler 需手动访问 ctx(不是 
ctx.Request.Body)。
app.UseFormOptions(...) 或至少注册了 Microsoft.AspNetCore.Http.Features.IFormFeature 支持(.NET 6+ 默认启用)ctx.Request.HasFormContentType 判断是否为表单请求,再调用 ctx.Request.ReadFormAsync() 获取 FormCollection
form["xmlFile"](假设前端字段名为 xmlFile)获取 IFormFile 实例;若字段名含空格或特殊字符,要用 form.Files["key"] 形式访问IFormFile.OpenReadStream() 后反复调用 —— 流只能读一次,且 Giraffe 管道可能已部分消费F# 中解析 XML 推荐用 System.Xml.XmlReader(而非 XDocument.Load),因为它支持流式读取、禁用 DTD、可设最大节点深度和缓冲区大小,这对上传场景至关重要。
XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit,防止 XML 外部实体(XXE)攻击MaxCharactersInDocument(如 10 * 1024 * 1024)限制总字符数,防超大文件耗尽内存use reader = XmlReader.Create(stream, settings) 确保及时释放资源;F# 的 use 绑定比 try/finally 更简洁可靠XElement.Load(stream) —— 它会一次性加载整个 XML 到内存,对上传文件极不安全比起生成强类型类(如用 XSD 工具),更符合 F# 风格的是用 XmlReader 驱动的递归解析函数,配合 discriminated union 表达结构。
type Person = { Name: string; Age: int }
type XmlError = | InvalidXml of string | MissingField of string
let parsePerson (reader: XmlReader) : Result =
try
let rec loop acc =
match reader.Read() with
| false -> Error (InvalidXml "Unexpected end of stream")
| true when reader.NodeType = XmlNodeType.Element && reader.Name = "Name" ->
let name = reader.ReadElementContentAsString()
loop { acc with Name = name }
| true when reader.NodeType = XmlNodeType.Element && reader.Name = "Age" ->
let age = reader.ReadElementContentAsInt()
loop { acc with Age = age }
| true when reader.NodeType = XmlNodeType.EndElement && reader.Name = "Person" ->
Ok acc
| _ -> loop acc
loop { Name = ""; Age = 0 }
with
| ex -> Error (InvalidXml ex.Message)
Result 而非抛异常,便于在 Giraffe handler 中统一处理错误路径(如 RequestErrors.BAD_REQUEST)一个典型的 Giraffe handler 应包含:表单检查 → 文件存在性验证 → 内容类型检查(application/xml 或 text/xml)→ 安全解析 → 错误映射。
let xmlUploadHandler : HttpHandler =
fun next ctx ->
task {
if not ctx.Request.HasFormContentType then
return! RequestErrors.BAD_REQUEST "Expected multipart/form-data" next ctx
let! form = ctx.Request.ReadFormAsync()
let file = form.Files.[|"xmlFile"|] // 注意索引是 string array
if isNull file || file.Length = 0L then
return! RequestErrors.BAD_REQUEST "No XML file uploaded" next ctx
if not (file.ContentType.Contains "xml") then
return! RequestErrors.BAD_REQUEST "File must be XML" next ctx
use stream = file.OpenReadStream()
let result = parsePerson (XmlReader.Create(stream, xmlSettings))
match result with
| Ok person -youjiankuohaophpcn
// 存库、发消息等后续逻辑
return! Successful.OK ("Uploaded: " + person.Name) next ctx
| Error err -youjiankuohaophpcn
return! RequestErrors.BAD_REQUEST (sprintf "Parse failed: %A" err) next ctx
}file.ContentType 比扩展名更可信,但也要校验(用户可伪造)file.Length 是字节长度,上传前可用它快速拒绝超限文件(比如 >5MB 直接 400)ReadFormAsync、OpenReadStream)都必须用 task { ... } 包裹,否则会阻塞线程Startup.fs 或 Program.fs 中注册 AddControllers(即使不用 MVC)—— 因为 IFormFile 绑定依赖 MVC 的服务注册最易被忽略的一点:上传临时文件不会自动清理,Giraffe 不接管生命周期。你得自己用 Path.GetTempFileName() + File.Delete 或内存流替代磁盘缓存,否则服务器磁盘会悄悄填满。