Skip to content

Commit c38d6a0

Browse files
committed
Add support for process_fragment_response callback and enhance fragment handling
- Introduced a request field in the Fragment struct to retain the original request. - Updated the Processor to utilize the provided fragment response processor. - Added tests to verify behavior with is_escaped_content configuration and response processing.
1 parent 2e3d5d6 commit c38d6a0

File tree

2 files changed

+489
-51
lines changed

2 files changed

+489
-51
lines changed

esi/src/lib.rs

Lines changed: 106 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ impl PendingFragmentContent {
334334
struct Fragment {
335335
/// Index in the chunks array where this fragment should be inserted
336336
chunk_index: usize,
337+
/// The original request for this fragment (needed for process_fragment_response callback)
338+
request: Request,
337339
/// The pending fragment content (request or already completed)
338340
pending_content: PendingFragmentContent,
339341
/// Alternative source URL if main request fails (built lazily only if needed)
@@ -502,8 +504,8 @@ impl Processor {
502504
self,
503505
mut src_document: impl BufRead,
504506
output_writer: &mut impl Write,
505-
_dispatch_fragment_request: Option<&FragmentRequestDispatcher>,
506-
_process_fragment_response: Option<&FragmentResponseProcessor>,
507+
dispatch_fragment_request: Option<&FragmentRequestDispatcher>,
508+
process_fragment_response: Option<&FragmentResponseProcessor>,
507509
) -> Result<()> {
508510
// If there is a source request to mimic, copy its metadata, otherwise use a default request.
509511
let original_request_metadata = self.original_request_metadata.as_ref().map_or_else(
@@ -543,7 +545,10 @@ impl Processor {
543545
// This allows us to dispatch all requests concurrently
544546
let mut fragments = Vec::new();
545547

546-
if let Some(dispatcher) = _dispatch_fragment_request {
548+
// Use the provided dispatcher or fall back to the default
549+
let dispatcher = dispatch_fragment_request.unwrap_or(&default_fragment_dispatcher);
550+
551+
if true {
547552
for (idx, chunk) in chunks.iter().enumerate() {
548553
if let parser_types::Chunk::Esi(parser_types::Tag::Include {
549554
src,
@@ -561,29 +566,73 @@ impl Processor {
561566
self.configuration.is_escaped_content,
562567
)?;
563568

564-
match dispatcher(req) {
569+
match dispatcher(req.clone_without_body()) {
565570
Ok(pending_content) => {
566571
fragments.push(Fragment {
567572
chunk_index: idx,
573+
request: req,
568574
pending_content,
569575
alt: alt.clone(),
570576
continue_on_error: *continue_on_error,
571577
});
572578
}
573579
Err(err) => {
574-
if !continue_on_error {
575-
return Err(ESIError::ExpressionError(format!(
576-
"Fragment dispatch failed: {}",
577-
err
578-
)));
580+
// Main request failed during dispatch
581+
// Try alt if available
582+
if let Some(alt_src) = alt {
583+
let interpolated_alt =
584+
try_evaluate_interpolated_string(alt_src, &mut ctx)?;
585+
let alt_req = build_fragment_request(
586+
original_request_metadata.clone_without_body(),
587+
&interpolated_alt,
588+
self.configuration.is_escaped_content,
589+
)?;
590+
591+
match dispatcher(alt_req.clone_without_body()) {
592+
Ok(alt_pending) => {
593+
fragments.push(Fragment {
594+
chunk_index: idx,
595+
request: alt_req,
596+
pending_content: alt_pending,
597+
alt: None, // Alt already used
598+
continue_on_error: *continue_on_error,
599+
});
600+
}
601+
Err(_) => {
602+
// Both main and alt failed
603+
if *continue_on_error {
604+
fragments.push(Fragment {
605+
chunk_index: idx,
606+
request: req,
607+
pending_content: PendingFragmentContent::NoContent,
608+
alt: None,
609+
continue_on_error: *continue_on_error,
610+
});
611+
} else {
612+
return Err(ESIError::ExpressionError(format!(
613+
"Fragment dispatch failed: {}",
614+
err
615+
)));
616+
}
617+
}
618+
}
619+
} else {
620+
// No alt, check if we should continue
621+
if *continue_on_error {
622+
fragments.push(Fragment {
623+
chunk_index: idx,
624+
request: req,
625+
pending_content: PendingFragmentContent::NoContent,
626+
alt: None,
627+
continue_on_error: *continue_on_error,
628+
});
629+
} else {
630+
return Err(ESIError::ExpressionError(format!(
631+
"Fragment dispatch failed: {}",
632+
err
633+
)));
634+
}
579635
}
580-
// If continue_on_error, we'll handle this during output phase
581-
fragments.push(Fragment {
582-
chunk_index: idx,
583-
pending_content: PendingFragmentContent::NoContent,
584-
alt: alt.clone(),
585-
continue_on_error: *continue_on_error,
586-
});
587636
}
588637
}
589638
}
@@ -595,38 +644,54 @@ impl Processor {
595644
let mut fragment_responses: std::collections::HashMap<usize, Result<Vec<u8>>> =
596645
std::collections::HashMap::new();
597646

598-
for fragment_info in fragments {
647+
for mut fragment_info in fragments {
599648
let response_result = fragment_info.pending_content.wait();
600649

650+
// Apply the fragment response processor if provided
651+
let processed_response = match response_result {
652+
Ok(resp) => {
653+
if let Some(processor) = process_fragment_response {
654+
processor(&mut fragment_info.request, resp)
655+
} else {
656+
Ok(resp)
657+
}
658+
}
659+
Err(e) => Err(e),
660+
};
661+
601662
// If main request failed and we have an alt, try the alt
602-
let final_response = if response_result.is_err() && fragment_info.alt.is_some() {
603-
if let Some(dispatcher) = _dispatch_fragment_request {
604-
let alt = fragment_info.alt.unwrap();
605-
let interpolated_alt = try_evaluate_interpolated_string(&alt, &mut ctx)?;
606-
let alt_req = build_fragment_request(
607-
original_request_metadata.clone_without_body(),
608-
&interpolated_alt,
609-
self.configuration.is_escaped_content,
610-
)?;
663+
let final_response = if processed_response.is_err() && fragment_info.alt.is_some() {
664+
let alt = fragment_info.alt.unwrap();
665+
let interpolated_alt = try_evaluate_interpolated_string(&alt, &mut ctx)?;
666+
let mut alt_req = build_fragment_request(
667+
original_request_metadata.clone_without_body(),
668+
&interpolated_alt,
669+
self.configuration.is_escaped_content,
670+
)?;
611671

612-
match dispatcher(alt_req) {
613-
Ok(alt_pending) => alt_pending.wait(),
614-
Err(e) => {
615-
if fragment_info.continue_on_error {
616-
Ok(Response::from_status(StatusCode::NO_CONTENT))
617-
} else {
618-
Err(ESIError::ExpressionError(format!(
619-
"Alt fragment failed: {}",
620-
e
621-
)))
622-
}
672+
match dispatcher(alt_req.clone_without_body()) {
673+
Ok(alt_pending) => {
674+
let alt_resp = alt_pending.wait()?;
675+
// Also process the alt response
676+
if let Some(processor) = process_fragment_response {
677+
processor(&mut alt_req, alt_resp)
678+
} else {
679+
Ok(alt_resp)
680+
}
681+
}
682+
Err(e) => {
683+
if fragment_info.continue_on_error {
684+
Ok(Response::from_status(StatusCode::NO_CONTENT))
685+
} else {
686+
Err(ESIError::ExpressionError(format!(
687+
"Alt fragment failed: {}",
688+
e
689+
)))
623690
}
624691
}
625-
} else {
626-
response_result
627692
}
628693
} else {
629-
response_result
694+
processed_response
630695
};
631696

632697
// Convert response to bytes immediately
@@ -692,22 +757,12 @@ fn default_fragment_dispatcher(req: Request) -> Result<PendingFragmentContent> {
692757
Ok(PendingFragmentContent::PendingRequest(pending_req))
693758
}
694759

695-
// Simple HTML entity decoder for common entities
696-
fn decode_html_entities(input: &str) -> String {
697-
input
698-
.replace("&lt;", "<")
699-
.replace("&gt;", ">")
700-
.replace("&amp;", "&")
701-
.replace("&quot;", "\"")
702-
.replace("&apos;", "'")
703-
}
704-
705760
// Helper function to build a fragment request from a URL
706761
// For HTML content the URL is unescaped if it's escaped (default).
707762
// It can be disabled in the processor configuration for a non-HTML content.
708763
fn build_fragment_request(mut request: Request, url: &str, is_escaped: bool) -> Result<Request> {
709764
let escaped_url = if is_escaped {
710-
decode_html_entities(url)
765+
html_escape::decode_html_entities(url).into_owned()
711766
} else {
712767
url.to_string()
713768
};

0 commit comments

Comments
 (0)