refactor some additional errors
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
f51d4237c7
commit
2e3e14b045
13 changed files with 127 additions and 148 deletions
|
@ -1,10 +1,9 @@
|
|||
use std::{io::Cursor, time::SystemTime};
|
||||
|
||||
use conduit::{debug, utils, warn, Error, Result};
|
||||
use conduit::{debug, utils, warn, Err, Result};
|
||||
use conduit_core::implement;
|
||||
use image::ImageReader as ImgReader;
|
||||
use ipaddress::IPAddress;
|
||||
use ruma::api::client::error::ErrorKind;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
use webpage::HTML;
|
||||
|
@ -89,7 +88,7 @@ pub async fn get_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
|
|||
async fn request_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
|
||||
if let Ok(ip) = IPAddress::parse(url) {
|
||||
if !self.services.globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
|
||||
return Err!(BadServerResponse("Requesting from this address is forbidden"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +98,7 @@ async fn request_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
|
|||
if let Some(remote_addr) = response.remote_addr() {
|
||||
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
|
||||
if !self.services.globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
|
||||
return Err!(BadServerResponse("Requesting from this address is forbidden"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,12 +108,12 @@ async fn request_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
|
|||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|x| x.to_str().ok())
|
||||
else {
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Unknown Content-Type"));
|
||||
return Err!(Request(Unknown("Unknown Content-Type")));
|
||||
};
|
||||
let data = match content_type {
|
||||
html if html.starts_with("text/html") => self.download_html(url).await?,
|
||||
img if img.starts_with("image/") => self.download_image(url).await?,
|
||||
_ => return Err(Error::BadRequest(ErrorKind::Unknown, "Unsupported Content-Type")),
|
||||
_ => return Err!(Request(Unknown("Unsupported Content-Type"))),
|
||||
};
|
||||
|
||||
self.set_url_preview(url, &data).await?;
|
||||
|
@ -142,7 +141,7 @@ async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
|
|||
}
|
||||
let body = String::from_utf8_lossy(&bytes);
|
||||
let Ok(html) = HTML::from_string(body.to_string(), Some(url.to_owned())) else {
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Failed to parse HTML"));
|
||||
return Err!(Request(Unknown("Failed to parse HTML")));
|
||||
};
|
||||
|
||||
let mut data = match html.opengraph.images.first() {
|
||||
|
|
|
@ -3,7 +3,7 @@ mod data;
|
|||
use std::{fmt::Debug, mem, sync::Arc};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use conduit::{debug_info, info, trace, utils::string_from_bytes, warn, Error, PduEvent, Result};
|
||||
use conduit::{debug_error, err, trace, utils::string_from_bytes, warn, Err, PduEvent, Result};
|
||||
use ipaddress::IPAddress;
|
||||
use ruma::{
|
||||
api::{
|
||||
|
@ -84,8 +84,9 @@ impl Service {
|
|||
let http_request = request
|
||||
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(""), &VERSIONS)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to find destination {dest} for push gateway: {e}");
|
||||
Error::BadServerResponse("Invalid push gateway destination")
|
||||
err!(BadServerResponse(warn!(
|
||||
"Failed to find destination {dest} for push gateway: {e}"
|
||||
)))
|
||||
})?
|
||||
.map(BytesMut::freeze);
|
||||
|
||||
|
@ -95,7 +96,7 @@ impl Service {
|
|||
trace!("Checking request URL for IP");
|
||||
if let Ok(ip) = IPAddress::parse(url_host) {
|
||||
if !self.services.globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
|
||||
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +111,7 @@ impl Service {
|
|||
if let Some(remote_addr) = response.remote_addr() {
|
||||
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
|
||||
if !self.services.globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
|
||||
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,10 +130,10 @@ impl Service {
|
|||
let body = response.bytes().await?; // TODO: handle timeout
|
||||
|
||||
if !status.is_success() {
|
||||
info!("Push gateway {dest} returned unsuccessful HTTP response ({status})");
|
||||
debug_info!("Push gateway response body: {:?}", string_from_bytes(&body));
|
||||
|
||||
return Err(Error::BadServerResponse("Push gateway returned unsuccessful response"));
|
||||
debug_error!("Push gateway response body: {:?}", string_from_bytes(&body));
|
||||
return Err!(BadServerResponse(error!(
|
||||
"Push gateway {dest} returned unsuccessful HTTP response: {status}"
|
||||
)));
|
||||
}
|
||||
|
||||
let response = T::IncomingResponse::try_from_http_response(
|
||||
|
@ -140,13 +141,11 @@ impl Service {
|
|||
.body(body)
|
||||
.expect("reqwest body is valid http body"),
|
||||
);
|
||||
response.map_err(|e| {
|
||||
warn!("Push gateway {dest} returned invalid response bytes: {e}");
|
||||
Error::BadServerResponse("Push gateway returned bad/invalid response")
|
||||
})
|
||||
response
|
||||
.map_err(|e| err!(BadServerResponse(error!("Push gateway {dest} returned invalid response: {e}"))))
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Could not send request to pusher {dest}: {e}");
|
||||
debug_error!("Could not send request to pusher {dest}: {e}");
|
||||
Err(e.into())
|
||||
},
|
||||
}
|
||||
|
@ -165,7 +164,7 @@ impl Service {
|
|||
.room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.map(|ev| {
|
||||
serde_json::from_str(ev.content.get())
|
||||
.map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
|
||||
.map_err(|e| err!(Database("invalid m.room.power_levels event: {e:?}")))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
@ -181,8 +180,8 @@ impl Service {
|
|||
};
|
||||
|
||||
if notify.is_some() {
|
||||
return Err(Error::bad_database(
|
||||
r#"Malformed pushrule contains more than one of these actions: ["dont_notify", "notify", "coalesce"]"#,
|
||||
return Err!(Database(
|
||||
r#"Malformed pushrule contains more than one of these actions: ["dont_notify", "notify", "coalesce"]"#
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use conduit::{debug, debug_error, debug_info, debug_warn, trace, Err, Error, Result};
|
||||
use conduit::{debug, debug_error, debug_info, debug_warn, err, trace, Err, Result};
|
||||
use hickory_resolver::{error::ResolveError, lookup::SrvLookup};
|
||||
use ipaddress::IPAddress;
|
||||
use ruma::ServerName;
|
||||
|
@ -329,10 +329,8 @@ impl super::Service {
|
|||
dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
|
||||
"Destination is not an IP literal."
|
||||
);
|
||||
let ip = IPAddress::parse(dest.host()).map_err(|e| {
|
||||
debug_error!("Failed to parse IP literal from string: {}", e);
|
||||
Error::BadServerResponse("Invalid IP address")
|
||||
})?;
|
||||
let ip = IPAddress::parse(dest.host())
|
||||
.map_err(|e| err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}"))))?;
|
||||
|
||||
self.validate_ip(&ip)?;
|
||||
|
||||
|
@ -341,7 +339,7 @@ impl super::Service {
|
|||
|
||||
pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
|
||||
if !self.services.globals.valid_cidr_range(ip) {
|
||||
return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
|
||||
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use conduit::{pdu::gen_event_id_canonical_json, warn, Err, Error, Result};
|
||||
use ruma::{api::client::error::ErrorKind, CanonicalJsonObject, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use conduit::{debug_warn, err, pdu::gen_event_id_canonical_json, Err, Result};
|
||||
use ruma::{CanonicalJsonObject, OwnedEventId, OwnedRoomId, RoomId};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
impl super::Service {
|
||||
pub fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<(OwnedEventId, CanonicalJsonObject, OwnedRoomId)> {
|
||||
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
|
||||
warn!("Error parsing incoming event {pdu:?}: {e:?}");
|
||||
Error::BadServerResponse("Invalid PDU in server response")
|
||||
debug_warn!("Error parsing incoming event {pdu:#?}");
|
||||
err!(BadServerResponse("Error parsing incoming event {e:?}"))
|
||||
})?;
|
||||
|
||||
let room_id: OwnedRoomId = value
|
||||
.get("room_id")
|
||||
.and_then(|id| RoomId::parse(id.as_str()?).ok())
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid room id in pdu"))?;
|
||||
.ok_or(err!(Request(InvalidParam("Invalid room id in pdu"))))?;
|
||||
|
||||
let Ok(room_version_id) = self.services.state.get_room_version(&room_id) else {
|
||||
return Err!("Server is not in room {room_id}");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{fmt::Debug, mem};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use conduit::{debug_error, trace, utils, warn, Error, Result};
|
||||
use conduit::{debug_error, err, trace, utils, warn, Err, Result};
|
||||
use reqwest::Client;
|
||||
use ruma::api::{appservice::Registration, IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
|
||||
|
||||
|
@ -26,10 +26,7 @@ where
|
|||
let hs_token = registration.hs_token.as_str();
|
||||
let mut http_request = request
|
||||
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(hs_token), &VERSIONS)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to find destination {dest}: {e}");
|
||||
Error::BadServerResponse("Invalid appservice destination")
|
||||
})?
|
||||
.map_err(|e| err!(BadServerResponse(warn!("Failed to find destination {dest}: {e}"))))?
|
||||
.map(BytesMut::freeze);
|
||||
|
||||
let mut parts = http_request.uri().clone().into_parts();
|
||||
|
@ -69,13 +66,11 @@ where
|
|||
let body = response.bytes().await?; // TODO: handle timeout
|
||||
|
||||
if !status.is_success() {
|
||||
warn!(
|
||||
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));
|
||||
return Err!(BadServerResponse(error!(
|
||||
"Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}",
|
||||
registration.id
|
||||
);
|
||||
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));
|
||||
|
||||
return Err(Error::BadServerResponse("Appservice returned unsuccessful HTTP response"));
|
||||
)));
|
||||
}
|
||||
|
||||
let response = T::IncomingResponse::try_from_http_response(
|
||||
|
@ -85,7 +80,9 @@ where
|
|||
);
|
||||
|
||||
response.map(Some).map_err(|e| {
|
||||
warn!("Appservice \"{}\" returned invalid response bytes {dest}: {e}", registration.id);
|
||||
Error::BadServerResponse("Appservice returned bad/invalid response")
|
||||
err!(BadServerResponse(error!(
|
||||
"Appservice \"{}\" returned invalid response bytes {dest}: {e}",
|
||||
registration.id
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{fmt::Debug, mem};
|
||||
|
||||
use conduit::{
|
||||
debug, debug_error, debug_warn, error::inspect_debug_log, trace, utils::string::EMPTY, Err, Error, Result,
|
||||
debug, debug_error, debug_warn, err, error::inspect_debug_log, trace, utils::string::EMPTY, Err, Error, Result,
|
||||
};
|
||||
use http::{header::AUTHORIZATION, HeaderValue};
|
||||
use ipaddress::IPAddress;
|
||||
|
@ -62,7 +62,7 @@ impl super::Service {
|
|||
trace!("Preparing request");
|
||||
let mut http_request = req
|
||||
.try_into_http_request::<Vec<u8>>(&actual.string, SATIR, &VERSIONS)
|
||||
.map_err(|_| Error::BadServerResponse("Invalid destination"))?;
|
||||
.map_err(|e| err!(BadServerResponse("Invalid destination: {e:?}")))?;
|
||||
|
||||
sign_request::<T>(&self.services.globals, dest, &mut http_request);
|
||||
|
||||
|
@ -139,10 +139,7 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
match response {
|
||||
Err(_) => Err(Error::BadServerResponse("Server returned bad 200 response.")),
|
||||
Ok(response) => Ok(response),
|
||||
}
|
||||
response.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
|
||||
}
|
||||
|
||||
fn handle_error<T>(
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use conduit::{debug, error, info, trace, warn, Error, Result};
|
||||
use conduit::{debug, debug_error, debug_warn, err, error, info, trace, warn, Err, Result};
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use ruma::{
|
||||
api::federation::{
|
||||
|
@ -57,13 +57,13 @@ impl Service {
|
|||
for event in events {
|
||||
for (signature_server, signature) in event
|
||||
.get("signatures")
|
||||
.ok_or(Error::BadServerResponse("No signatures in server response pdu."))?
|
||||
.ok_or(err!(BadServerResponse("No signatures in server response pdu.")))?
|
||||
.as_object()
|
||||
.ok_or(Error::BadServerResponse("Invalid signatures object in server response pdu."))?
|
||||
.ok_or(err!(BadServerResponse("Invalid signatures object in server response pdu.")))?
|
||||
{
|
||||
let signature_object = signature.as_object().ok_or(Error::BadServerResponse(
|
||||
let signature_object = signature.as_object().ok_or(err!(BadServerResponse(
|
||||
"Invalid signatures content object in server response pdu.",
|
||||
))?;
|
||||
)))?;
|
||||
|
||||
for signature_id in signature_object.keys() {
|
||||
server_key_ids
|
||||
|
@ -94,10 +94,12 @@ impl Service {
|
|||
.map(|(signature_server, signature_ids)| async {
|
||||
let fetch_res = self
|
||||
.fetch_signing_keys_for_server(
|
||||
signature_server.as_str().try_into().map_err(|_| {
|
||||
signature_server.as_str().try_into().map_err(|e| {
|
||||
(
|
||||
signature_server.clone(),
|
||||
Error::BadServerResponse("Invalid servername in signatures of server response pdu."),
|
||||
err!(BadServerResponse(
|
||||
"Invalid servername in signatures of server response pdu: {e:?}"
|
||||
)),
|
||||
)
|
||||
})?,
|
||||
signature_ids.into_iter().collect(), // HashSet to Vec
|
||||
|
@ -107,7 +109,9 @@ impl Service {
|
|||
match fetch_res {
|
||||
Ok(keys) => Ok((signature_server, keys)),
|
||||
Err(e) => {
|
||||
warn!("Signature verification failed: Could not fetch signing key for {signature_server}: {e}",);
|
||||
debug_error!(
|
||||
"Signature verification failed: Could not fetch signing key for {signature_server}: {e}",
|
||||
);
|
||||
Err((signature_server, e))
|
||||
},
|
||||
}
|
||||
|
@ -123,7 +127,7 @@ impl Service {
|
|||
.insert(signature_server.clone(), keys);
|
||||
},
|
||||
Err((signature_server, e)) => {
|
||||
warn!("Failed to fetch keys for {}: {:?}", signature_server, e);
|
||||
debug_warn!("Failed to fetch keys for {signature_server}: {e:?}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -141,35 +145,37 @@ impl Service {
|
|||
pub_key_map: &mut RwLockWriteGuard<'_, BTreeMap<String, BTreeMap<String, Base64>>>,
|
||||
) -> Result<()> {
|
||||
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
|
||||
error!("Invalid PDU in server response: {:?}: {:?}", pdu, e);
|
||||
Error::BadServerResponse("Invalid PDU in server response")
|
||||
debug_error!("Invalid PDU in server response: {pdu:#?}");
|
||||
err!(BadServerResponse(error!("Invalid PDU in server response: {e:?}")))
|
||||
})?;
|
||||
|
||||
let signatures = value
|
||||
.get("signatures")
|
||||
.ok_or(Error::BadServerResponse("No signatures in server response pdu."))?
|
||||
.ok_or(err!(BadServerResponse("No signatures in server response pdu.")))?
|
||||
.as_object()
|
||||
.ok_or(Error::BadServerResponse("Invalid signatures object in server response pdu."))?;
|
||||
.ok_or(err!(BadServerResponse("Invalid signatures object in server response pdu.")))?;
|
||||
|
||||
for (signature_server, signature) in signatures {
|
||||
let signature_object = signature.as_object().ok_or(Error::BadServerResponse(
|
||||
let signature_object = signature.as_object().ok_or(err!(BadServerResponse(
|
||||
"Invalid signatures content object in server response pdu.",
|
||||
))?;
|
||||
)))?;
|
||||
|
||||
let signature_ids = signature_object.keys().cloned().collect::<Vec<_>>();
|
||||
|
||||
let contains_all_ids =
|
||||
|keys: &BTreeMap<String, Base64>| signature_ids.iter().all(|id| keys.contains_key(id));
|
||||
|
||||
let origin = <&ServerName>::try_from(signature_server.as_str())
|
||||
.map_err(|_| Error::BadServerResponse("Invalid servername in signatures of server response pdu."))?;
|
||||
let origin = <&ServerName>::try_from(signature_server.as_str()).map_err(|e| {
|
||||
err!(BadServerResponse(
|
||||
"Invalid servername in signatures of server response pdu: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
if servers.contains_key(origin) || pub_key_map.contains_key(origin.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Loading signing keys for {}", origin);
|
||||
|
||||
debug!("Loading signing keys for {origin}");
|
||||
let result: BTreeMap<_, _> = self
|
||||
.services
|
||||
.globals
|
||||
|
@ -179,7 +185,7 @@ impl Service {
|
|||
.collect();
|
||||
|
||||
if !contains_all_ids(&result) {
|
||||
debug!("Signing key not loaded for {}", origin);
|
||||
debug_warn!("Signing key not loaded for {origin}");
|
||||
servers.insert(origin.to_owned(), BTreeMap::new());
|
||||
}
|
||||
|
||||
|
@ -196,7 +202,7 @@ impl Service {
|
|||
pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
|
||||
) -> Result<()> {
|
||||
for server in self.services.globals.trusted_servers() {
|
||||
debug!("Asking batch signing keys from trusted server {}", server);
|
||||
debug!("Asking batch signing keys from trusted server {server}");
|
||||
match self
|
||||
.services
|
||||
.sending
|
||||
|
@ -209,14 +215,16 @@ impl Service {
|
|||
.await
|
||||
{
|
||||
Ok(keys) => {
|
||||
debug!("Got signing keys: {:?}", keys);
|
||||
debug!("Got signing keys: {keys:?}");
|
||||
let mut pkm = pub_key_map.write().await;
|
||||
for k in keys.server_keys {
|
||||
let k = match k.deserialize() {
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
warn!("Received error {e} while fetching keys from trusted server {server}");
|
||||
warn!("{}", k.into_json());
|
||||
warn!(
|
||||
"Received error {e} while fetching keys from trusted server {server}: {:#?}",
|
||||
k.into_json()
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
@ -236,13 +244,10 @@ impl Service {
|
|||
pkm.insert(k.server_name.to_string(), result);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed sending batched key request to trusted key server {server} for the remote servers \
|
||||
{:?}: {e}",
|
||||
servers
|
||||
);
|
||||
},
|
||||
Err(e) => error!(
|
||||
"Failed sending batched key request to trusted key server {server} for the remote servers \
|
||||
{servers:?}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -478,7 +483,6 @@ impl Service {
|
|||
}
|
||||
} else {
|
||||
info!("query_trusted_key_servers_first is set to false, querying {origin} first");
|
||||
|
||||
debug!("Asking {origin} for their signing keys over federation");
|
||||
if let Some(server_key) = self
|
||||
.services
|
||||
|
@ -536,7 +540,7 @@ impl Service {
|
|||
.filter_map(|e| e.deserialize().ok())
|
||||
.collect::<Vec<_>>()
|
||||
}) {
|
||||
debug!("Got signing keys: {:?}", server_keys);
|
||||
debug!("Got signing keys: {server_keys:?}");
|
||||
for k in server_keys {
|
||||
self.services
|
||||
.globals
|
||||
|
@ -561,7 +565,6 @@ impl Service {
|
|||
}
|
||||
}
|
||||
|
||||
warn!("Failed to find public key for server: {origin}");
|
||||
Err(Error::BadServerResponse("Failed to find public key for server"))
|
||||
Err!(BadServerResponse(warn!("Failed to find public key for server {origin:?}")))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue